diff --git a/.codecov.yml b/.codecov.yml index 8888c4b7..fdd5e587 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -6,5 +6,5 @@ coverage: threshold: 1.0% base: auto branches: - - main - - dev + - main + - dev diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ebbc37f8..b657c0fb 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -8,13 +8,16 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} ARG NODE_VERSION="none" RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi +# Poetry +ARG POETRY_VERSION="none" +ENV POETRY_VIRTUALENVS_IN_PROJECT=true +RUN if [ "${POETRY_VERSION}" != "none" ]; then su vscode -c "umask 0002 && pip3 install poetry==${POETRY_VERSION}"; fi + # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. -COPY requirements.txt /tmp/pip-tmp/ -COPY requirements-build.txt /tmp/pip-tmp/ -COPY requirements-dev.txt /tmp/pip-tmp/ +# COPY requirements.txt /tmp/pip-tmp/ -RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements-dev.txt \ - && rm -rf /tmp/pip-tmp +# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ +# && rm -rf /tmp/pip-tmp # [Optional] Uncomment this section to install additional OS packages. # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 06f59a4f..7ef05733 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,51 +9,43 @@ // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6 // Append -bullseye or -buster to pin to an OS version. // Use -bullseye variants on local on arm64/Apple Silicon. - "VARIANT": "3.10" + "VARIANT": "3.10", // Options - // "NODE_VERSION": "lts/*" + // "NODE_VERSION": "lts/*", + "POETRY_VERSION": "1.5.1" } }, - // "runArgs": [ - // "--env-file", - // ".env" - // ], // Set *default* container specific settings.json values on container create. "customizations": { "vscode": { "settings": { - "python.defaultInterpreterPath": "/usr/local/bin/python", - "python.linting.enabled": true, - "python.linting.flake8Enabled": true, - "python.linting.flake8Args": [ - "--max-line-length=79" - ], - "python.linting.pylintEnabled": false, - "python.formatting.provider": "autopep8", - "python.formatting.autopep8Args": [ - "--in-place", - "--max-line-length=79" - ], - "python.testing.pytestEnabled": true, + "python.defaultInterpreterPath": "./.venv/bin/python", + "python.terminal.activateEnvironment": true, + "python.terminal.activateEnvInCurrentTerminal": true, + "editor.rulers": [79], "python.testing.pytestArgs": [ "tests" ], - "isort.check": true + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ "ms-python.python", - "njpwerner.autodocstring" + "GitHub.vscode-pull-request-github" ] } }, // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], + "forwardPorts": [ + 8000 + ], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "pre-commit install --install-hooks", + "postCreateCommand": "poetry install && poetry run pre-commit install --install-hooks", // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode", "features": { - // "git": "latest" + // "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {} } } diff --git a/.dockerignore b/.dockerignore index f1696849..9760fb1e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,20 @@ -__pycache__ -*/.env -*/config.ini -*/scanner.log +# Ignore everything +* + +# Allow files and directories +!/docker +!/requirements.txt +!/tgtg_scanner +!/pyproject.toml +!/poetry.lock +!/README.md +!/LICENSE + +# Ignore unnecessary files inside allowed directories +# This should go after the allowed directories +**/.DS_Store +**/Thumbs.db +**/__pycache__/ +**/.ipynb_checkpoints/ +**/config.ini +**/scanner.log diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1c6aee97..19915c29 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,10 +5,10 @@ version: 2 updates: - - package-ecosystem: "pip" # See documentation for possible values - directory: "/" # Location of package manifests - schedule: - interval: "daily" - target-branch: "dev" - assignees: - - "Der-Henning" +- package-ecosystem: pip # See documentation for possible values + directory: / # Location of package manifests + schedule: + interval: daily + target-branch: dev + assignees: + - Der-Henning diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 56a1b95b..3b9b9899 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,11 +1,14 @@ -### Pull Request Checklist: + +### Pull Request Checklist -* [ ] Have you checked to ensure there aren't other open [Pull Requests](../../pulls) for the same update/change? +* [ ] Have you checked to ensure there aren't other open +[Pull Requests](../../pulls) for the same update/change? * [ ] Did you make your Pull Request on the dev branch? * [ ] Does your submission pass tests? `make test` * [ ] Have you lint your code locally prior to submission? `make lint` * [ ] Could you build and run the docker image successfully? `make image` * [ ] Could you create a running executable? `make executable` -* [ ] Have you added an explanation of what your changes do and why you'd like to include them? +* [ ] Have you added an explanation of what your changes do +and why you'd like to include them? * [ ] Have you written new tests for your changes, as applicable? * [ ] Have you successfully ran manual tests with your changes locally? diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 565ca691..8986ca32 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -4,7 +4,7 @@ on: push: pull_request: schedule: - - cron: "0 1 * * *" + - cron: 0 1 * * * jobs: code-ql: @@ -15,8 +15,8 @@ jobs: contents: read security-events: write steps: - - uses: actions/checkout@v3 - - uses: github/codeql-action/init@v2 - with: - languages: "python" - - uses: github/codeql-action/analyze@v2 + - uses: actions/checkout@v3 + - uses: github/codeql-action/init@v2 + with: + languages: python + - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/publish-wiki.yml b/.github/workflows/publish-wiki.yml new file mode 100644 index 00000000..a5b68047 --- /dev/null +++ b/.github/workflows/publish-wiki.yml @@ -0,0 +1,25 @@ +name: Publish wiki + +on: + push: + branches: [main] + paths: + - wiki/** + - .github/workflows/publish-wiki.yml + +concurrency: + group: publish-wiki + cancel-in-progress: true + +permissions: + contents: write + +jobs: + publish-wiki: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: impresscms-dev/strip-markdown-extensions-from-links-action@v1.0.0 + with: + path: wiki + - uses: Andrew-Chen-Wang/github-wiki-action@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e064cd4..7fcd1fc0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,101 +3,134 @@ name: Releases on: push: branches: - - main + - main tags: - - v* + - v* + pull_request: + +env: + PYTHON_VERSION: '3.10' + POETRY_VERSION: 1.5.1 + jobs: docker-images: name: Build Docker Images runs-on: ubuntu-latest + defaults: + run: + shell: bash strategy: + fail-fast: ${{ github.event_name == 'push' }} matrix: include: - - base: slim - tag: "" - file: ./Dockerfile - context: ./ - - base: alpine - tag: -alpine - file: ./Dockerfile.alpine - context: ./ + - base: slim + suffix: '' + file: ./docker/Dockerfile + context: ./ + - base: alpine + suffix: -alpine + file: ./docker/Dockerfile.alpine + context: ./ steps: - - uses: actions/checkout@v3 - - uses: docker/metadata-action@v4 - id: meta - with: - images: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPOSITORY }} - flavor: | - suffix=${{ matrix.tag }},onlatest=true - tags: | - type=edge,branch=main,suffix=${{ matrix.tag }} - type=semver,pattern=v{{version}} - type=semver,pattern=v{{major}}.{{minor}} - type=semver,pattern=v{{major}} - - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - uses: docker/setup-qemu-action@v2 - - uses: docker/setup-buildx-action@v2 - id: buildx - - uses: docker/build-push-action@v3 - with: - context: ${{ matrix.context }} - file: ${{ matrix.file }} - platforms: linux/arm64, linux/amd64, linux/arm/v7, linux/386, linux/arm/v6 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - uses: peter-evans/dockerhub-description@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - repository: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPOSITORY }} - short-description: ${{ github.event.repository.description }} - readme-filepath: ./DOCKER_README.md + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Setup Poetry + run: | + pip install --upgrade pip setuptools wheel poetry==${{ env.POETRY_VERSION }} + poetry config virtualenvs.create true --local + poetry config virtualenvs.in-project true --local + - name: Export requirements.txt + run: poetry export -f requirements.txt --output requirements.txt + - uses: docker/metadata-action@v4 + id: meta + with: + images: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPOSITORY }} + flavor: | + suffix=${{ matrix.suffix }},onlatest=true + tags: | + type=edge,branch=main,suffix=${{ matrix.suffix }} + type=semver,pattern=v{{version}} + type=semver,pattern=v{{major}}.{{minor}} + type=semver,pattern=v{{major}} + - uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - uses: docker/setup-qemu-action@v2 + - uses: docker/setup-buildx-action@v2 + id: buildx + - uses: docker/build-push-action@v3 + with: + context: ${{ matrix.context }} + file: ${{ matrix.file }} + platforms: linux/arm64, linux/amd64, linux/arm/v7, linux/386, linux/arm/v6 + push: ${{ github.event_name == 'push' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + - uses: peter-evans/dockerhub-description@v3 + if: github.event_name == 'push' + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + repository: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPOSITORY }} + short-description: ${{ github.event.repository.description }} + readme-filepath: ./docker/DOCKER_README.md + releases: name: Build Release Files runs-on: ${{ matrix.os }} strategy: + fail-fast: ${{ github.event_name == 'push' }} matrix: include: - - os: ubuntu-latest - tag: linux - - os: windows-latest - tag: win - - os: macos-latest - tag: macos + - os: ubuntu-latest + tag: linux + - os: windows-latest + tag: win + - os: macos-latest + tag: macos steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - name: Install dependencies - run: pip install -r requirements-build.txt - - name: Run PyInstaller - run: | - pyinstaller scanner.spec - cp ./src/config.sample.ini ./dist/config.ini - cp ./README.md ./dist/README.md - cp ./LICENSE ./dist/LICENSE - - name: Make filename for archive - id: filename - shell: bash - run: echo "FILENAME=scanner-${{ github.ref_name }}-${{ matrix.tag }}.zip" >> $GITHUB_OUTPUT - - name: Zip files (linux/macos) - if: matrix.tag == 'linux' || matrix.tag == 'macos' - run: zip -j ./${{ steps.filename.outputs.FILENAME }} ./dist/* - - name: Zip files (win) - if: matrix.tag == 'win' - run: Compress-Archive ./dist/* ./${{ steps.filename.outputs.FILENAME }} - - name: Upload archive - uses: actions/upload-artifact@v3 - with: - name: releases - path: ./${{ steps.filename.outputs.FILENAME }} - - name: Add archive to release - uses: softprops/action-gh-release@v1 - if: github.ref_type == 'tag' - with: - files: ./${{ steps.filename.outputs.FILENAME }} + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Setup Poetry + run: | + pip install --upgrade pip setuptools wheel poetry==${{ env.POETRY_VERSION }} + poetry config virtualenvs.create true --local + poetry config virtualenvs.in-project true --local + - uses: actions/cache@v3 + with: + path: ./.venv + key: venv-build-poetry-${{ env.POETRY_VERSION }}-python-${{ env.PYTHON_VERSION }}-${{ hashFiles('poetry.lock') }}-${{ runner.os }} + - name: Install dependencies + run: poetry install --without test + - name: Run PyInstaller + run: | + poetry run pyinstaller scanner.spec + cp ./config.sample.ini ./dist/config.ini + cp ./README.md ./dist/README.md + cp ./LICENSE ./dist/LICENSE + ./dist/scanner -v + - name: Make filename for archive + id: filename + shell: bash + run: echo "FILENAME=scanner-${{ github.ref_name }}-${{ matrix.tag }}.zip" | sed -r 's,/,-,g' >> $GITHUB_OUTPUT + - name: Zip files (linux/macos) + if: matrix.tag == 'linux' || matrix.tag == 'macos' + run: zip -j ./${{ steps.filename.outputs.FILENAME }} ./dist/* + - name: Zip files (win) + if: matrix.tag == 'win' + run: Compress-Archive ./dist/* ./${{ steps.filename.outputs.FILENAME }} + - name: Upload archive + uses: actions/upload-artifact@v3 + with: + name: releases + path: ./${{ steps.filename.outputs.FILENAME }} + - name: Add archive to release + uses: softprops/action-gh-release@v1 + if: github.ref_type == 'tag' + with: + files: ./${{ steps.filename.outputs.FILENAME }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fec3f96a..9172edd5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,25 +3,41 @@ name: Tests on: pull_request: +env: + POETRY_VERSION: 1.5.1 + jobs: tests: name: Run Tests runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - os: ["ubuntu-latest", "windows-latest", "macos-latest"] - python: ["3.9", "3.10", "3.11"] + os: [ubuntu-latest, windows-latest, macos-latest] + python: ['3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python }} - - name: Install dependencies - run: | - pip install -r requirements.txt - pip install pytest-cov pytest-mock responses pre-commit - - name: Run linting - run: pre-commit run -a - - name: Run tests - run: python -m pytest -v -m "not tgtg_api" --cov src/ --cov-report=xml - - uses: codecov/codecov-action@v3 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + - name: Setup Poetry + run: | + pip install --upgrade pip setuptools wheel poetry==${{ env.POETRY_VERSION }} + poetry config virtualenvs.create true --local + poetry config virtualenvs.in-project true --local + poetry config installer.max-workers 1 --local + - uses: actions/cache@v3 + with: + path: ./.venv + key: venv-test-poetry-${{ env.POETRY_VERSION }}-python-${{ matrix.python }}-${{ hashFiles('poetry.lock') }}-${{ runner.os }} + - uses: actions/cache@v3 + with: + path: ~/.cache/pre-commit + key: pre-commit-python-${{ matrix.python }}-${{ hashFiles('poetry.lock', '.pre-commit-config.yaml') }}-${{ runner.os }} + - name: Install dependencies + run: poetry install --no-interaction --without build + - name: Run linting + run: poetry run pre-commit run -a + - name: Run tests + run: poetry run pytest -v -m "not tgtg_api" --cov --cov-report=xml + - uses: codecov/codecov-action@v3 diff --git a/.github/workflows/tgtg.yml b/.github/workflows/tgtg.yml index 6fd07aa8..ca9d805d 100644 --- a/.github/workflows/tgtg.yml +++ b/.github/workflows/tgtg.yml @@ -2,64 +2,75 @@ name: TGTG API Test on: schedule: - - cron: "0 1 * * *" + - cron: 0 1 * * * + +env: + PYTHON_VERSION: '3.10' + POETRY_VERSION: 1.5.1 jobs: test: name: Run Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - name: Install dependencies - run: | - pip install -r requirements.txt - pip install pytest-cov responses - - name: Run tests - uses: nick-fields/retry@v2 - with: - timeout_minutes: 10 - max_attempts: 10 - retry_wait_seconds: 1800 - command: | - python -m pytest -v -m tgtg_api - echo "::add-mask::${{ env.TGTG_ACCESS_TOKEN }}" - echo "::add-mask::${{ env.TGTG_REFRESH_TOKEN }}" - echo "::add-mask::${{ env.TGTG_USER_ID }}" - echo "::add-mask::${{ env.TGTG_COOKIE }}" - echo "TGTG_ACCESS_TOKEN=${{ env.TGTG_ACCESS_TOKEN }}" >> $GITHUB_ENV - echo "TGTG_REFRESH_TOKEN=${{ env.TGTG_REFRESH_TOKEN }}" >> $GITHUB_ENV - echo "TGTG_USER_ID=${{ env.TGTG_USER_ID }}" >> $GITHUB_ENV - echo "TGTG_COOKIE=${{ env.TGTG_COOKIE }}" >> $GITHUB_ENV - env: - TGTG_USERNAME: ${{ secrets.TGTG_USERNAME }} - TGTG_ACCESS_TOKEN: ${{ secrets.TGTG_ACCESS_TOKEN }} - TGTG_REFRESH_TOKEN: ${{ secrets.TGTG_REFRESH_TOKEN }} - TGTG_USER_ID: ${{ secrets.TGTG_USER_ID }} - TGTG_COOKIE: ${{ secrets.TGTG_COOKIE }} - - uses: Der-Henning/actions-set-secret@v2.1.0 - with: - name: "TGTG_ACCESS_TOKEN" - value: ${{ env.TGTG_ACCESS_TOKEN }} - repository: ${{ github.repository }} - token: ${{ secrets.REPO_ACCESS_TOKEN }} - - uses: Der-Henning/actions-set-secret@v2.1.0 - with: - name: "TGTG_REFRESH_TOKEN" - value: ${{ env.TGTG_REFRESH_TOKEN }} - repository: ${{ github.repository }} - token: ${{ secrets.REPO_ACCESS_TOKEN }} - - uses: Der-Henning/actions-set-secret@v2.1.0 - with: - name: "TGTG_USER_ID" - value: ${{ env.TGTG_USER_ID }} - repository: ${{ github.repository }} - token: ${{ secrets.REPO_ACCESS_TOKEN }} - - uses: Der-Henning/actions-set-secret@v2.1.0 - with: - name: "TGTG_COOKIE" - value: ${{ env.TGTG_COOKIE }} - repository: ${{ github.repository }} - token: ${{ secrets.REPO_ACCESS_TOKEN }} + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Setup Poetry + run: | + pip install --upgrade pip setuptools wheel poetry==${{ env.POETRY_VERSION }} + poetry config virtualenvs.create true --local + poetry config virtualenvs.in-project true --local + - uses: actions/cache@v3 + with: + path: ./.venv + key: venv-test-poetry-${{ env.POETRY_VERSION }}-python-${{ env.PYTHON_VERSION }}-${{ hashFiles('poetry.lock') }}-${{ runner.os }} + - name: Install dependencies + run: poetry install --without build + - name: Run tests + uses: nick-fields/retry@v2 + with: + timeout_minutes: 10 + max_attempts: 10 + retry_wait_seconds: 1800 + command: | + poetry run pytest -v -m tgtg_api + echo "::add-mask::${{ env.TGTG_ACCESS_TOKEN }}" + echo "::add-mask::${{ env.TGTG_REFRESH_TOKEN }}" + echo "::add-mask::${{ env.TGTG_USER_ID }}" + echo "::add-mask::${{ env.TGTG_COOKIE }}" + echo "TGTG_ACCESS_TOKEN=${{ env.TGTG_ACCESS_TOKEN }}" >> $GITHUB_ENV + echo "TGTG_REFRESH_TOKEN=${{ env.TGTG_REFRESH_TOKEN }}" >> $GITHUB_ENV + echo "TGTG_USER_ID=${{ env.TGTG_USER_ID }}" >> $GITHUB_ENV + echo "TGTG_COOKIE=${{ env.TGTG_COOKIE }}" >> $GITHUB_ENV + env: + TGTG_USERNAME: ${{ secrets.TGTG_USERNAME }} + TGTG_ACCESS_TOKEN: ${{ secrets.TGTG_ACCESS_TOKEN }} + TGTG_REFRESH_TOKEN: ${{ secrets.TGTG_REFRESH_TOKEN }} + TGTG_USER_ID: ${{ secrets.TGTG_USER_ID }} + TGTG_COOKIE: ${{ secrets.TGTG_COOKIE }} + - uses: Der-Henning/actions-set-secret@v2.1.0 + with: + name: TGTG_ACCESS_TOKEN + value: ${{ env.TGTG_ACCESS_TOKEN }} + repository: ${{ github.repository }} + token: ${{ secrets.REPO_ACCESS_TOKEN }} + - uses: Der-Henning/actions-set-secret@v2.1.0 + with: + name: TGTG_REFRESH_TOKEN + value: ${{ env.TGTG_REFRESH_TOKEN }} + repository: ${{ github.repository }} + token: ${{ secrets.REPO_ACCESS_TOKEN }} + - uses: Der-Henning/actions-set-secret@v2.1.0 + with: + name: TGTG_USER_ID + value: ${{ env.TGTG_USER_ID }} + repository: ${{ github.repository }} + token: ${{ secrets.REPO_ACCESS_TOKEN }} + - uses: Der-Henning/actions-set-secret@v2.1.0 + with: + name: TGTG_COOKIE + value: ${{ env.TGTG_COOKIE }} + repository: ${{ github.repository }} + token: ${{ secrets.REPO_ACCESS_TOKEN }} diff --git a/.gitignore b/.gitignore index 0dd482f2..cd47c740 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,12 @@ dist .vscode .DS_Store .venv +venv .ipynb_checkpoints .coverage coverage.xml .pytest_cache +requirements.txt .env config.ini diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 00000000..ea921d84 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,12 @@ +# Default state for all rules +default: true + +# MD013/line-length - Line length +MD013: + line_length: 120 + tables: false + +# MD041/first-line-heading/first-line-h1 - First line in a file should be a top-level heading +MD041: + # Heading level + level: 1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3cbd5a0e..fa1ee02d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,36 +1,54 @@ exclude: (build|dist) repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 - hooks: - - id: trailing-whitespace - - id: check-added-large-files - - id: check-case-conflict - - id: detect-private-key - - id: requirements-txt-fixer - - id: end-of-file-fixer - - id: check-yaml - - id: check-added-large-files +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: check-added-large-files + - id: check-case-conflict + - id: detect-private-key + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: check-added-large-files - - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v2.0.2 - hooks: - - id: autopep8 - files: ^(src|tests)/ - args: - - --in-place - - --max-line-length=79 +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v2.0.2 + hooks: + - id: autopep8 + args: + - --in-place + - --max-line-length=79 - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort - files: ^(src|tests)/ +- repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 - hooks: - - id: flake8 - files: ^(src|tests)/ - args: - - --max-line-length=79 +- repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + args: + - --max-line-length=79 + +- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks + rev: v2.9.0 + hooks: + - id: pretty-format-toml + args: [--autofix] + exclude: poetry.lock + - id: pretty-format-yaml + args: [--autofix, --indent, '2'] + +- repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.35.0 + hooks: + - id: markdownlint + args: [--fix] + +- repo: https://github.com/python-poetry/poetry + rev: 1.5.1 # add version here + hooks: + - id: poetry-check + - id: poetry-lock diff --git a/DOCKER_README.md b/DOCKER_README.md deleted file mode 100644 index ef3583b7..00000000 --- a/DOCKER_README.md +++ /dev/null @@ -1,105 +0,0 @@ -# Quick reference - -Readme, source, and documentation on [https://github.com/Der-Henning/tgtg](https://github.com/Der-Henning/tgtg). - -# Supported Tags and respective `Dockerfile` links - - The `latest` images represent the latest stable release. - The `edge` images contain the latest commits to the main branch. - The `alpine` images are based on the alpine Linux distribution and are significantly smaller. - -- [`edge`](https://github.com/Der-Henning/tgtg/blob/main/Dockerfile) -- [`edge-alpine`](https://github.com/Der-Henning/tgtg/blob/main/Dockerfile.alpine) -- [`v1`, `v1.17`, `v1.17.1`, `latest`](https://github.com/Der-Henning/tgtg/blob/v1.17.1/Dockerfile) -- [`v1-alpine`, `v1.17-alpine`, `v1.17.1-alpine`, `latest-alpine`](https://github.com/Der-Henning/tgtg/blob/v1.17.1/Dockerfile.alpine) - -# Quick Start - -**Docker Compose Example:** - -````xml -version: "3.3" - -services: - tgtg: - image: derhenning/tgtg:latest - environment: - - TZ=Europe/Berlin - - DEBUG=false - - TGTG_USERNAME= - - SLEEP_TIME=60 - #- SCHEDULE_CRON= - #- ITEM_IDS= - - METRICS=false - - METRICS_PORT=8000 - - DISABLE_TESTS=false - - QUIET=false - - LOCALE=en_US - - - LOCATION=false - - LOCATION_GOOGLE_MAPS_API_KEY = - - LOCATION_ADDRESS = - - - APPRISE=false - - APPRISE_URL= - - APPRISE_BODY= - #- APPRISE_TITLE= - #- APPRISE_CRON= - - - SMTP=false - - SMTP_HOST=smtp.gmail.com - - SMTP_PORT=465 - - SMTP_USERNAME=max.mustermann@gmail.com - - SMTP_PASSWORD= - - SMTP_TLS=true - - SMTP_SENDER=max.mustermann@gmail.com - - SMTP_RECIPIENT=max.mustermann@gmail.com - #-SMTP_SUBJECT= - #-SMTP_BODY= - - - PUSH_SAFER=false - - PUSH_SAFER_KEY= - - PUSH_SAFER_DEVICE_ID= - - - TELEGRAM=false - - TELEGRAM_TOKEN= - - TELEGRAM_CHAT_IDS= - #- TELEGRAM_TIMEOUT=60 - #- TELEGRAM_BODY= - - - SCRIPT=false - - SCRIPT_COMMAND= - #- SCRIPT_CRON= - - - IFTTT=false - - IFTTT_EVENT=tgtg_notification - - IFTTT_KEY= - - - NTFY=false - - NTFY_SERVER=https://ntfy.sh - - NTFY_TOPIC= - #- NTFY_TITLE - #- NTFY_BODY= - #- NTFY_PRIORITY= - #- NTFY_TAGS= - #- NTFY_USERNAME= - #- NTFY_PASSWORD= - #- NTFY_TIMEOUT=60 - #- NTFY_CRON= - - - WEBHOOK=false - - WEBHOOK_URL= - - WEBHOOK_METHOD=POST - - WEBHOOK_BODY= - - WEBHOOK_TYPE=plain/text - #- WEBHOOK_HEADERS= - #- WEBHOOK_USERNAME= - #- WEBHOOK_PASSWORD= - #- WEBHOOK_TIMEOUT=60 - #- WEBHOOK_CRON= - volumes: - - tokens:/tokens - -volumes: - tokens: -```` diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index c21e28eb..00000000 --- a/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM python:3.10-slim - -RUN addgroup --gid 1001 tgtg && \ - adduser --shell /bin/false --disabled-password --uid 1001 --gid 1001 tgtg -RUN mkdir -p /app -RUN chown tgtg:tgtg /app -RUN mkdir -p /tokens -RUN chown tgtg:tgtg /tokens -ENV TGTG_TOKEN_PATH=/tokens -ENV DOCKER=true -ENV PYTHONUNBUFFERED=1 - -VOLUME /tokens -WORKDIR /app -USER tgtg - -COPY --chown=tgtg:tgtg requirements.txt /tmp/pip-tmp/ -RUN pip3 install --disable-pip-version-check --no-cache-dir \ - --no-warn-script-location -r /tmp/pip-tmp/requirements.txt \ - && rm -rf /tmp/pip-tmp - -COPY --chown=tgtg:tgtg ./src . - -CMD [ "python", "-u", "main.py" ] diff --git a/Dockerfile.alpine b/Dockerfile.alpine deleted file mode 100644 index 954e74f9..00000000 --- a/Dockerfile.alpine +++ /dev/null @@ -1,24 +0,0 @@ -FROM python:3.10-alpine - -RUN addgroup --gid 1001 --system tgtg && \ - adduser --shell /bin/false --disabled-password --uid 1001 --system --ingroup tgtg tgtg -RUN mkdir -p /app -RUN chown tgtg:tgtg /app -RUN mkdir -p /tokens -RUN chown tgtg:tgtg /tokens -ENV TGTG_TOKEN_PATH=/tokens -ENV DOCKER=true -ENV PYTHONUNBUFFERED=1 - -VOLUME /tokens -WORKDIR /app -USER tgtg - -COPY --chown=tgtg:tgtg requirements.txt /tmp/pip-tmp/ -RUN pip3 install --disable-pip-version-check --no-cache-dir \ - --no-warn-script-location -r /tmp/pip-tmp/requirements.txt \ - && rm -rf /tmp/pip-tmp - -COPY --chown=tgtg:tgtg ./src . - -CMD [ "python", "-u", "main.py" ] diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index 88558464..00000000 --- a/Dockerfile.dev +++ /dev/null @@ -1,14 +0,0 @@ -FROM python:3.10 - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt update && apt install -y zip && rm -rf /var/lib/apt/lists/* - -COPY requirements.txt /tmp/pip-tmp/ -COPY requirements-dev.txt /tmp/pip-tmp/ - -RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements-dev.txt \ - && rm -rf /tmp/pip-tmp - -RUN mkdir -p /tokens -VOLUME /tokens -ENV TGTG_TOKEN_PATH=/tokens diff --git a/Makefile b/Makefile index 67ba51bf..18370624 100644 --- a/Makefile +++ b/Makefile @@ -1,28 +1,23 @@ -image: - docker build -f Dockerfile -t tgtg-scanner:latest . +images: + poetry export -f requirements.txt --output requirements.txt + docker build -f ./docker/Dockerfile -t tgtg-scanner:latest . + docker build -f ./docker/Dockerfile.alpine -t tgtg-scanner:latest-alpine . install: - pip install -r requirements-dev.txt + poetry install start: - python src/main.py - -bash: - docker-compose -f docker-compose.dev.yml build - docker-compose -f docker-compose.dev.yml run --rm bash + poetry run scanner -d executable: - rm -r build ||: - rm -r dist ||: - pyinstaller scanner.spec - cp src/config.sample.ini dist/config.ini - zip -j dist/scanner.zip dist/* + rm -r ./build ||: + rm -r ./dist ||: + poetry run pyinstaller ./scanner.spec + cp ./config.sample.ini ./dist/config.ini + zip -j ./dist/scanner.zip ./dist/* test: - python -m pytest -v -m "not tgtg_api" --cov src/ + poetry run pytest -v -m "not tgtg_api" --cov lint: - pre-commit run -a - -clean: - docker-compose -f docker-compose.dev.yml down --remove-orphans + poetry run pre-commit run -a diff --git a/README.md b/README.md index 8694f95d..570aa9ca 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,53 @@ +# TGTG Scanner + [![Tests](https://github.com/Der-Henning/tgtg/actions/workflows/tests.yml/badge.svg)](https://github.com/Der-Henning/tgtg/actions/workflows/tests.yml) [![codecov](https://codecov.io/github/Der-Henning/tgtg/branch/main/graph/badge.svg?token=POHW9USW7C)](https://codecov.io/github/Der-Henning/tgtg) [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/der-henning/tgtg/release.yml)](https://github.com/Der-Henning/tgtg/actions/workflows/release.yml) [![GitHub release](https://img.shields.io/github/release/Der-Henning/tgtg?include_prereleases=&sort=semver&color=blue)](https://github.com/Der-Henning/tgtg/releases/) [![Docker Pulls](https://img.shields.io/docker/pulls/derhenning/tgtg)](https://hub.docker.com/r/derhenning/tgtg) -# TGTG Scanner - -TGTG Scanner observes your favorite TGTG Magic Bags for newly available items and notifies you via mail, IFTTT, Ntfy, Telegram, PushSafer, Apprise or any other WebHook. Notifications will be sent when the available amount of Magic Bags rises from zero to something. +TGTG Scanner observes your favorite TGTG Magic Bags for newly available items and notifies you +via mail, IFTTT, Ntfy, Telegram, PushSafer, Apprise or any other WebHook. +Notifications will be sent when the available amount of Magic Bags rises from zero to something. Additionally, the currently available amounts can be provided via an HTTP server. -Running in a docker container the scanner can be seamlessly integrated with OpenHab, Prometheus, and other automation, notification, and visualization services. +Running in a docker container the scanner can be seamlessly integrated with +OpenHab, Prometheus, and other automation, notification, and visualization services. -This software is provided as is without warranty of any kind. If you have problems, find bugs, or have suggestions for improvement feel free to create an issue or contribute to the project. Before creating an issue please refer to the [FAQ](https://github.com/Der-Henning/tgtg/wiki/FAQ). +This software is provided as is without warranty of any kind. +If you have problems, find bugs, or have suggestions for improvement +feel free to create an issue or contribute to the project. +Before creating an issue please refer to the [FAQ](https://github.com/Der-Henning/tgtg/wiki/FAQ). ## Disclaimer -This Project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Too Good To Go, or any of its subsidiaries or its affiliates. +This Project is not affiliated, associated, authorized, endorsed by, or in any way +officially connected with Too Good To Go, or any of its subsidiaries or its affiliates. -Too Good To Go explicitly forbids the use of their platform the way this tool does if you use it. In their Terms and Conditions, it says: "The Consumer must not misuse the Platform (including hacking or 'scraping')." +Too Good To Go explicitly forbids the use of their platform the way this tool does if you use it. +In their Terms and Conditions, it says: +"The Consumer must not misuse the Platform (including hacking or 'scraping')." -If you use this tool you do it at your own risk. Too Good To Go may stop you from doing so by (temporarily) blocking your access and may even delete your account. +If you use this tool you do it at your own risk. +Too Good To Go may stop you from doing so by (temporarily) blocking your access +and may even delete your account. ## Error 403 -If you see the Error 403 in your logs please refer to the [FAQ](https://github.com/Der-Henning/tgtg/wiki/FAQ#1-i-am-getting-error-403-all-the-time). +If you see the Error 403 in your logs please refer to the +[FAQ](https://github.com/Der-Henning/tgtg/wiki/FAQ#1-i-am-getting-error-403-all-the-time). ## Installation -You can install this tool on any computer. For 24/7 notifications I recommended an installation on a NAS like Synology or a Raspberry Pi. You can also try to use a virtual cloud server. +You can install this tool on any computer. +For 24/7 notifications I recommended an installation on a NAS like Synology or a Raspberry Pi. +You can also try to use a virtual cloud server. If you have any problems or questions feel free to create an issue. -For configuration options please refer to the projects wiki: [Configuration](https://github.com/Der-Henning/tgtg/wiki/Configuration) +For configuration options please refer to the projects wiki: +[Configuration](https://github.com/Der-Henning/tgtg/wiki/Configuration) You have the following three options to install the scanner, ascending in complexity: @@ -40,7 +55,9 @@ You have the following three options to install the scanner, ascending in comple This is the simplest but least flexible solution suitable for most operating systems. -The binaries are built for latest Ubuntu, MacOS, and Windows running on an `x64` architecture. If you are using another architecture like `arm` (e.g. RaspberryPi, Synology, etc.) you have to run from source, compile the binary yourself or use the docker images. +The binaries are built for latest Ubuntu, MacOS, and Windows running on an `x64` architecture. +If you are using another architecture like `arm` (e.g. RaspberryPi, Synology, etc.) +you have to run from source, compile the binary yourself or use the docker images. 1. Download latest [Releases](https://github.com/Der-Henning/tgtg/releases) for your OS 2. Unzip the archive @@ -49,73 +66,110 @@ The binaries are built for latest Ubuntu, MacOS, and Windows running on an `x64` You can run the scanner manually if you need it, add it to your startup or create a system service. -The executables for Windows and MacOS are not signed by Microsoft and Apple, which would be very expensive. -On MacOS, you have to hold the control key while opening the file and on Windows, you have to confirm the displayed dialog. +The executables for Windows and MacOS are not signed by Microsoft and Apple, +which would be very expensive. +On MacOS, you have to hold the control key while opening the file and on Windows, +you have to confirm the displayed dialog. ### Run with Docker -My preferred method for servers, NAS, and RapsberryPis is using the pre-build multi-arch Linux images available via [Docker Hub](https://hub.docker.com/r/derhenning/tgtg). The images are built for Linux on `amd64`, `arm64`, `armv7`, `armv6`, and `i386`. +My preferred method for servers, NAS, and RapsberryPis is using the pre-build multi-arch Linux images available via +[Docker Hub](https://hub.docker.com/r/derhenning/tgtg). +The images are built for Linux on `amd64`, `arm64`, `armv7`, `armv6`, and `i386`. 1. Install Docker and docker-compose -2. Copy and edit `docker-compose.yml` as described in the [Wiki](https://github.com/Der-Henning/tgtg/wiki/Configuration) +2. Copy and edit `docker-compose.yml` as described in the +[Wiki](https://github.com/Der-Henning/tgtg/wiki/Configuration) 3. Run `docker-compose up -d` -The container automatically creates a volume mounting `\tokens` where the app saves the TGTG credentials after login. These credentials will be reused at every start of the container to avoid the mail login process. To log in with a different account you have to delete the created volume or the files in it. +The container automatically creates a volume mounting `\tokens` +where the app saves the TGTG credentials after login. +These credentials will be reused at every start of the container to avoid the mail login process. +To log in with a different account you have to delete the created volume or the files in it. To update the running container to the latest version of the selected tag run -````bash +```bash docker-compose pull docker-compose up -d -```` +``` + +### Install as package + +1. Install Git, Python>=3.9 and pip +2. Run `pip install git+https://github.com/Der-Henning/tgtg` +3. Create `config.ini` as described in the +[Wiki](https://github.com/Der-Henning/tgtg/wiki/Configuration) +4. Start scanner with `python -m tgtg_scanner` + +To update to the latest release run +`pip install --upgrade git+https://github.com/Der-Henning/tgtg`. + +If you receive the `ModuleNotFoundError: No module named '_ctypes'` +you may need to install `libffi-dev`. ### Run from source Method for advanced usage. -1. Install Git, Python>=3.9, and pip +1. Install Git, Python>=3.9 and poetry 2. Clone the repository `git clone https://github.com/Der-Henning/tgtg` 3. Enter repository folder `cd tgtg` -4. Run `pip install -r requirements.txt` -5. Create config file `cp src/config.sample.ini src/config.ini` -6. Modify `src/config.ini` as described in the [Wiki](https://github.com/Der-Henning/tgtg/wiki/Configuration) -7. Run `python src/main.py` +4. Run `poetry install --without test,build` +5. Create config file `cp config.sample.ini config.ini` +6. Modify `config.ini` as described in the +[Wiki](https://github.com/Der-Henning/tgtg/wiki/Configuration) +7. Run `poetry run scanner` -Alternatively, you can use environment variables as described in the `sample.env` file. The scanner will look for environment variables if no `config.ini` is present. +Alternatively, you can use environment variables as described in the wiki. +The scanner will look for environment variables if no `config.ini` is present. To update to the latest release run `git pull`. -If you receive the `ModuleNotFoundError: No module named '_ctypes'` you may need to install `libffi-dev`. +If you receive the `ModuleNotFoundError: No module named '_ctypes'` +you may need to install `libffi-dev`. ### Build your own binary You could also build your own binary for your OS/Arch combination. 1. Clone the repository as described above -2. Run `pip install -r requirements-build.txt` -3. Run `make executable` or `pyinstaller scanner.spec` +2. Run `poetry install --without test` +3. Run `make executable` You will find the bundled binary including the `config.ini` in the `./dist` directory. ## Usage -When the scanner is started it will first try to log in to your TGTG account. Similar to logging in to the TGTG app, you have to click on the link sent to you by mail. This won't work on your mobile phone if you have installed the TGTG app, so you have to check your mailbox on your PC. +When the scanner is started it will first try to log in to your TGTG account. +Similar to logging in to the TGTG app, you have to click on the link sent to you by mail. +This won't work on your mobile phone if you have installed the TGTG app, +so you have to check your mailbox on your PC. -After a successful login, the scanner will send a test notification on all configured notifiers. If you don't receive any notifications, please check your configuration. +After a successful login, the scanner will send a test notification on all configured notifiers. +If you don't receive any notifications, please check your configuration. ### Helper functions -The executable or the `src/main.py` contains some useful helper functions that can be accessed via optional command line arguments. Running `scanner[.exe] --help` or `python src/main.py --help` displays the available commands. +The executable or the `tgtg_scanner/__main__.py` contains some useful helper functions that can be +accessed via optional command line arguments. +Running `scanner[.exe] --help`, `poetry run scanner --help`, `python tgtg_scanner/__main__.py --help` +or `python -m tgtg_scanner --help` displays the available commands. -```` -usage: main.py [-h] [-v] [-d] [-t | -f | -F | -a item_id [item_id ...] | -r item_id [item_id ...] | -R] [-j | -J] + +```txt +usage: scanner [-h] [-v] [-d] [-c config_file] [-l log_file] [-t | -f | -F | -a item_id [item_id ...] | -r item_id [item_id ...] | -R] [-j | -J] -TooGoodToGo scanner and notifier. +Notifications for Too Good To Go options: -h, --help show this help message and exit - -v, --version shows the program's version number and exit + -v, --version show program's version number and exit -d, --debug activate debugging mode + -c config_file, --config config_file + path to config file (default: config.ini) + -l log_file, --log_file log_file + path to log file (default: scanner.log) -t, --tokens display your current access tokens and exit -f, --favorites display your favorites and exit -F, --favorite_ids display the item ids of your favorites and exit @@ -124,25 +178,27 @@ options: -r item_id [item_id ...], --remove item_id [item_id ...] remove item ids from favorites and exit -R, --remove_all remove all favorites and exit - -j, --json output as plain JSON - -J, --json_pretty output as pretty JSON -```` + -j, --json output as plain json + -J, --json_pretty output as pretty json +``` + Example (Unix only): -````bash -python src/main.py -f -J >> items.json -```` +```bash +poetry run scanner -f -J >> items.json +``` Creates a formatted JSON file containing all your favorite items and their available information. ### Metrics -Enabling the metrics option will expose an HTTP server on the specified port supplying the currently available items. You can scrape the data with Prometheus to create and visualize historic data or use it with your home automation. +Enabling the metrics option will expose an HTTP server on the specified port supplying the currently available items. +You can scrape the data with Prometheus to create and visualize historic data or use it with your home automation. Scrape config: -````xml +```xml - job_name: 'TGTG' scrape_interval: 1m scheme: http @@ -150,36 +206,40 @@ Scrape config: static_configs: - targets: - 'localhost:8000' -```` +``` ## Development For development, I recommend using docker. -If you are developing with VSCode, you can open the project in the configured development container including all required dependencies. +If you are developing with VSCode, you can open the project in the configured +development container including all required dependencies. Alternatively, install all required development environment dependencies, including linting, testing, and building by executing -````bash -pip install -r requirements-dev.txt -```` +```bash +poetry install +``` ### Makefile commands -- `make image` builds docker image with tag `tgtg-scanner:latest` +- `make images` builds docker images with tag `tgtg-scanner:latest` and `tgtg-scanner:latest-alpine` - `make install` installs development dependencies -- `make start` is short for `python src/main.py` -- `make bash` starts dev python docker image with installed dependencies and mounted project in bash +- `make start` is short for `poetry run scanner -d` - `make executable` creates a bundled executable in `/dist` - `make test` runs unit tests - `make lint` run pre-commit hooks -- `make clean` cleans up docker-compose ### Creating new notifiers -Feel free to create and contribute new notifiers for other services and endpoints. You can use an existing notifier as a template or build upon the webhook notifier. E.g. see the [ifttt notifier](https://github.com/Der-Henning/tgtg/blob/main/src/notifiers/ifttt.py). +Feel free to create and contribute new notifiers for other services and endpoints. +You can use an existing notifier as a template or build upon the webhook notifier. +E.g. see the [ifttt notifier](https://github.com/Der-Henning/tgtg/blob/main/src/notifiers/ifttt.py). --- If you want to support me, feel free to buy me a coffee. -Buy Me A Coffee + + +Buy Me A Coffee + diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md index d8cc9883..c8c64c48 100644 --- a/RELEASE_CHECKLIST.md +++ b/RELEASE_CHECKLIST.md @@ -1,11 +1,12 @@ # Release Checklist -Since I always forget something when I create a new release, here is a small checklist for my future me. +Since I always forget something when I create a new release, +here is a small checklist for my future me. * [ ] Read this checklist. * [ ] Make sure all tests are passing. * [ ] Do some manual testing. * [ ] Make sure everything that should be merged is merged. (Dependency updates?) -* [ ] Update Version in `./src/_version.py`. +* [ ] Update Version in `pyproject.toml`. * [ ] Update Version and Dockerfile links in `./DOCKER_README.md`. * [ ] Check if the readme file needs to be adjusted. diff --git a/src/config.sample.ini b/config.sample.ini similarity index 100% rename from src/config.sample.ini rename to config.sample.ini diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index 47f8db70..00000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: "3.3" -services: - bash: - build: - context: . - dockerfile: Dockerfile.dev - working_dir: /home/app - # env_file: - # - .env - volumes: - - .:/home/app - stdin_open: true - tty: true - command: /bin/bash diff --git a/docker-compose.yml b/docker-compose.yml index 75fa9baa..a40269e4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,115 +1,25 @@ -version: "3.3" +version: '3.3' + services: - app: - image: derhenning/tgtg:latest ## pre build image from docker hub + scanner: + image: derhenning/tgtg:latest-alpine ## pre build image from docker hub #image: tgtg-scanner:latest ## locally build image environment: - - TZ=Europe/Berlin ## Set timezone for pickupdate, otherwise will be utc - - DEBUG=false ## true for debug log messages - - TGTG_USERNAME= ## TGTG Username / Login EMail - - SLEEP_TIME=60 ## Time to wait till next scan in seconds - default 60 seconds - #- SCHEDULE_CRON= ## (optional) Scheduler in cron schedule expression - ## Example of cron schedule expression: - ## - SCHEDULE_CRON=* 12-14 * * 1-5 - ## => allowed to run at hours between 12:00 and 14:59 on monday to friday - ## more help with formatting at https://crontab.guru/#*_12-14_*_*_1-5 - ## The Scanner will not make any requests to the TGTG API in the excluded periods - - LOCATION=false ## true = calculate distance and travel time to the item location/store - - LOCATION_GOOGLE_MAPS_API_KEY= ## Google Maps API Key https://developers.google.com/maps/documentation/javascript/get-api-key - - LOCATION_ADDRESS= ## Home/Origin address - - - ITEM_IDS= ## (optional) Comma seperated list of Item Ids to scan - - METRICS=false ## Enable to export metrics for Prometheus - - METRICS_PORT=8000 ## Port for metrics http server - - DISABLE_TESTS=false ## true to disable test notifications on startup - - QUIET=false ## true to disable all console messages. Only errors and console notifier messages - - LOCALE=en_US ## set locale to localize ${{pickupdate}} - - - APPRISE=false ## true = enable notifications via Apprise - - APPRISE_URL= ## Apprise URL - ## See the list of supported services: https://github.com/caronc/apprise/wiki - #- APPRISE_CRON= - #- APPRISE_TITLE= ## - #- APPRISE_BODY= ## Message body, variables as described below - ## The supported message format depends on the chosen service - ## Default: '${{display_name}} - new amount: ${{items_available}} - https://share.toogoodtogo.com/item/${{item_id}}' - - - CONSOLE=false ## true = enable simple console notifications - #- CONSOLE_BODY= ## console message with variables as described below - #- CONSOLE_CRON= ## Disable notifications based on cron - - - SMTP=false ## true = enable mail notifications via smtp - - SMTP_HOST=smtp.gmail.com ## smtp Server - smtp.gmail.com for google - - SMTP_PORT=587 ## smtp Server Port - 587 for TLS, 465 for SSL, 25 for unsecured - - SMTP_USERNAME=max.mustermann@gmail.com ## smtp Login - your EMail for google - - SMTP_PASSWORD= ## smtp Login Password - your Login PW for google - - SMTP_TLS=true ## enable TLS - - SMTP_SSL=false ## enable SSL - - SMTP_SENDER=max.mustermann@gmail.com ## sender adress - same as Login for google and most smtp servers - - SMTP_RECIPIENT=max.mustermann@gmail.com ## recipient for notifications - can be the same as sender - #- SMTP_CRON= - #- SMTP_SUBJECT= ## Subject and Body options are optional. - #- SMTP_BODY= ## Subject and Body options can use variables as described below - ## The Body option is interpreted as HTML + # Configuration via environment variables. + # Basic example using Telegram notifications + # For more options and details visit https://github.com/Der-Henning/tgtg/wiki/Configuration - - PUSH_SAFER=false ## true = enable notifications via pushsafer.com - - PUSH_SAFER_KEY= - - PUSH_SAFER_DEVICE_ID= - #- PUSH_SAFER_CRON= + - TGTG_USERNAME= + - SLEEP_TIME=60 + - TZ=Europe/Berlin + - LOCALE=de_DE - - TELEGRAM=false ## true = enable notifications via Telegram - - TELEGRAM_TOKEN= ## Telegram Bot token - see @botfather - - TELEGRAM_CHAT_IDS= ## Telegram Chat id, multiple ids separated by comma - - TELEGRAM_TIMEOUT=60 ## Timeout for Telegram API requests - #- TELEGRAM_CRON= - #- TELEGRAM_BODY= ## Optional message body as markdown, variables as described below - ## Example: - ## 'TELEGRAM_BODY=*$${{display_name}}*\n*Available*: $${{items_available}}\n*Rating*: $${{rating}}\n*Price*: $${{price}} $${{currency}}\n*Pickup*: $${{pickupdate}}' - ## In some cases you may have to add - ## 'TELEGRAM_BODY={{` ... `}}' + - TELEGRAM=true + - TELEGRAM_TOKEN= + - TELEGRAM_CHAT_IDS= - - SCRIPT=false ## true = enable running a script - - SCRIPT_COMMAND= ## Path to custom script. Example: - ## SCRIPT_COMMAND=/home/user/tgtg/src/notify.sh - ## Make sure the script is executable! - #- SCRIPT_CRON= - - - IFTTT=false ## true = enable notifications via IFTTT Webhooks - - IFTTT_EVENT=tgtg_notification ## IFTTT Webhooks Event - - IFTTT_KEY= ## IFTTT Webhooks Key - #- IFTTT_CRON= - - IFTTT_BODY= ## IFTTT json data body - ## Default: {"value1": "$${{display_name}}", "value2": $${{items_available}}, "value3": "https://share.toogoodtogo.com/item/$${{item_id}}"} - - ## Possible $${{variable}} variables: item_id, items_available, display_name, price, currency, pickupdate, item_logo, item_cover, scanned_on - - NTFY=false ## true = enable notifications via ntfy - - NTFY_SERVER=https://ntfy.sh ## server to use for ntfy url - - NTFY_TOPIC= ## topic the topic to use for ntfy url - #- NTFY_TITLE=New TGTG Items ## Title of the notification - #- NTFY_BODY= ## Body of the notification - ## Default: ${{display_name}} - New Amount: ${{items_available}} - https://share.toogoodtogo.com/item/${{item_id}} - #- NTFY_PRIORITY=default ## Priority of the notification - #- NTFY_TAGS=tgtg ## Tags of the notification - #- NTFY_CLICK= ## URL to open on click - Default: https://share.toogoodtogo.com/item/${{item_id}} - #- NTFY_USERNAME= ## Username for the ntfy server/topic - #- NTFY_PASSWORD= ## Password for the ntfy server/topic - #- NTFY_TIMEOUT=60 ## Request Timeout - #- NTFY_CRON= - - ## Possible $${{variable}} variables: item_id, items_available, display_name, price, currency, pickupdate, item_logo, item_cover, scanned_on - ## Link to item in App: https://share.toogoodtogo.com/item/${{item_id}} - ## Example: - ## - 'WEBHOOK_BODY={"message": "$${{display_name}} - New Amount: $${{items_available}}", "priority": 2, "title": "New TGTG Items"}' - ## Notice that you have to enclose the variable with '' - - WEBHOOK=false ## true = enable notifications via costum WebHook - - WEBHOOK_URL= ## URL of your WebHook, can contain $${{variable}} - - WEBHOOK_METHOD=POST ## Request Method - - WEBHOOK_BODY={} ## Data to send, can contain $${{variable}} - - WEBHOOK_TYPE=application/json ## Content-Type for header, default: text/plain - - WEBHOOK_HEADERS={} ## Additional headers - - WEBHOOK_TIMEOUT=60 ## Request Timeout - #- WEBHOOK_CRON= volumes: - - tokens:/tokens ## volume to save TGTG credentials to reuse on next start up and avoid login mail + - tokens:/tokens ## volume to save TGTG credentials to reuse on next start up and avoid login mail + volumes: tokens: diff --git a/docker/DOCKER_README.md b/docker/DOCKER_README.md new file mode 100644 index 00000000..3f3059e5 --- /dev/null +++ b/docker/DOCKER_README.md @@ -0,0 +1,48 @@ +# Quick reference + +Readme, source, and documentation on [https://github.com/Der-Henning/tgtg](https://github.com/Der-Henning/tgtg). + + +# Supported Tags and respective `Dockerfile` links + + The `latest` images represent the latest stable release. + The `edge` images contain the latest commits to the main branch. + The `alpine` images are based on the alpine Linux distribution and are significantly smaller. + +- [`edge`](https://github.com/Der-Henning/tgtg/blob/main/docker/Dockerfile) +- [`edge-alpine`](https://github.com/Der-Henning/tgtg/blob/main/docker/Dockerfile.alpine) +- [`v1`, `v1.18`, `v1.18.0`, `latest`](https://github.com/Der-Henning/tgtg/blob/v1.18.0/docker/Dockerfile) +- [`v1-alpine`, `v1.18-alpine`, `v1.18.0-alpine`, `latest-alpine`](https://github.com/Der-Henning/tgtg/blob/v1.18.0/docker/Dockerfile.alpine) + + +# Quick Start + +**Docker Compose Example:** + +Basic example using Telegram notifications. + +For more options and details visit . + +````xml +version: '3.3' + +services: + scanner: + image: derhenning/tgtg:latest-alpine + + environment: + - TGTG_USERNAME= + - SLEEP_TIME=60 + - TZ=Europe/Berlin + - LOCALE=de_DE + + - TELEGRAM=true + - TELEGRAM_TOKEN= + - TELEGRAM_CHAT_IDS= + + volumes: + - tokens:/tokens + +volumes: + tokens: +```` diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..e324d57f --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,36 @@ +FROM python:3.10-slim + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_NO_WARN_SCRIPT_LOCATION=0 +ENV TGTG_TOKEN_PATH=/tokens +ENV LOGS_PATH=/logs +ENV DOCKER=true +ENV UID=1000 +ENV GID=1000 + +RUN addgroup --gid $GID tgtg && \ + adduser --shell /bin/false \ + --disabled-password \ + --uid $UID \ + --gid $GID \ + tgtg +RUN mkdir -p /logs +RUN mkdir -p /tokens +RUN chown tgtg:tgtg /tokens +RUN chown tgtg:tgtg /logs +VOLUME /tokens + +RUN --mount=type=bind,target=/context \ + pip install -r /context/requirements.txt && \ + pip install /context + +COPY ./docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +RUN python -m tgtg_scanner -v + +ENTRYPOINT [ "/entrypoint.sh" ] +CMD [ "python", "-m", "tgtg_scanner" ] diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine new file mode 100644 index 00000000..28a5d55f --- /dev/null +++ b/docker/Dockerfile.alpine @@ -0,0 +1,38 @@ +FROM python:3.10-alpine as base + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_NO_WARN_SCRIPT_LOCATION=0 +ENV TGTG_TOKEN_PATH=/tokens +ENV LOGS_PATH=/logs +ENV DOCKER=true +ENV UID=1000 +ENV GID=1000 + +RUN addgroup --gid $GID --system tgtg && \ + adduser --shell /bin/false \ + --disabled-password \ + --uid $UID \ + --system \ + --ingroup tgtg \ + tgtg +RUN mkdir -p /logs +RUN mkdir -p /tokens +RUN chown tgtg:tgtg /tokens +RUN chown tgtg:tgtg /logs +VOLUME /tokens + +RUN apk update && apk add --no-cache shadow runuser +RUN --mount=type=bind,target=/context \ + pip install -r /context/requirements.txt && \ + pip install /context + +COPY ./docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +RUN python -m tgtg_scanner -v + +ENTRYPOINT [ "/entrypoint.sh" ] +CMD [ "python", "-m", "tgtg_scanner" ] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 00000000..69897429 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -e + +echo "Updating UID and GID to ${UID}:${GID}" +usermod -u ${UID} tgtg && groupmod -g ${GID} tgtg +chown -R ${UID}:${GID} /tokens /logs + +echo "Starting tgtg" +exec runuser -u tgtg -- "$@" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..a7da09fd --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1048 @@ +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. + +[[package]] +name = "altgraph" +version = "0.17.3" +description = "Python graph (network) package" +optional = false +python-versions = "*" +files = [ + {file = "altgraph-0.17.3-py2.py3-none-any.whl", hash = "sha256:c8ac1ca6772207179ed8003ce7687757c04b0b71536f81e2ac5755c6226458fe"}, + {file = "altgraph-0.17.3.tar.gz", hash = "sha256:ad33358114df7c9416cdb8fa1eaa5852166c505118717021c6a8c7c7abbd03dd"}, +] + +[[package]] +name = "apprise" +version = "1.4.5" +description = "Push Notifications that work with just about every platform!" +optional = false +python-versions = ">=3.6" +files = [ + {file = "apprise-1.4.5-py3-none-any.whl", hash = "sha256:01c9949327d94c11c886bd1ae387ba7f61cdb9d6247b8096686920685e40fb47"}, + {file = "apprise-1.4.5.tar.gz", hash = "sha256:b7c66513c5456690a298ed887c9016ded42f15e365d16142e728b74f7cffee82"}, +] + +[package.dependencies] +certifi = "*" +click = ">=5.0" +markdown = "*" +PyYAML = "*" +requests = "*" +requests-oauthlib = "*" + +[[package]] +name = "apscheduler" +version = "3.6.3" +description = "In-process task scheduler with Cron-like capabilities" +optional = false +python-versions = "*" +files = [ + {file = "APScheduler-3.6.3-py2.py3-none-any.whl", hash = "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526"}, + {file = "APScheduler-3.6.3.tar.gz", hash = "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244"}, +] + +[package.dependencies] +pytz = "*" +setuptools = ">=0.7" +six = ">=1.4.0" +tzlocal = ">=1.2" + +[package.extras] +asyncio = ["trollius"] +doc = ["sphinx", "sphinx-rtd-theme"] +gevent = ["gevent"] +mongodb = ["pymongo (>=2.8)"] +redis = ["redis (>=3.0)"] +rethinkdb = ["rethinkdb (>=2.4.0)"] +sqlalchemy = ["sqlalchemy (>=0.8)"] +testing = ["mock", "pytest", "pytest-asyncio", "pytest-asyncio (<0.6)", "pytest-cov", "pytest-tornado5"] +tornado = ["tornado (>=4.3)"] +twisted = ["twisted"] +zookeeper = ["kazoo"] + +[[package]] +name = "cachetools" +version = "4.2.2" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = "~=3.5" +files = [ + {file = "cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001"}, + {file = "cachetools-4.2.2.tar.gz", hash = "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"}, +] + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.2.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, + {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, +] + +[[package]] +name = "click" +version = "8.1.6" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, + {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "colorlog" +version = "6.7.0" +description = "Add colours to the output of Python's logging module." +optional = false +python-versions = ">=3.6" +files = [ + {file = "colorlog-6.7.0-py2.py3-none-any.whl", hash = "sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662"}, + {file = "colorlog-6.7.0.tar.gz", hash = "sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +development = ["black", "flake8", "mypy", "pytest", "types-colorama"] + +[[package]] +name = "coverage" +version = "7.2.7" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cron-descriptor" +version = "1.4.0" +description = "A Python library that converts cron expressions into human readable strings." +optional = false +python-versions = "*" +files = [ + {file = "cron_descriptor-1.4.0.tar.gz", hash = "sha256:b6ff4e3a988d7ca04a4ab150248e9f166fb7a5c828a85090e75bcc25aa93b4dd"}, +] + +[package.extras] +dev = ["polib"] + +[[package]] +name = "distlib" +version = "0.3.7" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, + {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.12.2" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.7" +files = [ + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, +] + +[package.extras] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "googlemaps" +version = "4.10.0" +description = "Python client library for Google Maps Platform" +optional = false +python-versions = ">=3.5" +files = [ + {file = "googlemaps-4.10.0.tar.gz", hash = "sha256:3055fcbb1aa262a9159b589b5e6af762b10e80634ae11c59495bd44867e47d88"}, +] + +[package.dependencies] +requests = ">=2.20.0,<3.0" + +[[package]] +name = "humanize" +version = "4.7.0" +description = "Python humanize utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "humanize-4.7.0-py3-none-any.whl", hash = "sha256:df7c429c2d27372b249d3f26eb53b07b166b661326e0325793e0a988082e3889"}, + {file = "humanize-4.7.0.tar.gz", hash = "sha256:7ca0e43e870981fa684acb5b062deb307218193bca1a01f2b2676479df849b3a"}, +] + +[package.extras] +tests = ["freezegun", "pytest", "pytest-cov"] + +[[package]] +name = "identify" +version = "2.5.26" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.26-py2.py3-none-any.whl", hash = "sha256:c22a8ead0d4ca11f1edd6c9418c3220669b3b7533ada0a0ffa6cc0ef85cf9b54"}, + {file = "identify-2.5.26.tar.gz", hash = "sha256:7243800bce2f58404ed41b7c002e53d4d22bcf3ae1b7900c2d7aefd95394bf7f"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.8.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, + {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "macholib" +version = "1.16.2" +description = "Mach-O header analysis and editing" +optional = false +python-versions = "*" +files = [ + {file = "macholib-1.16.2-py2.py3-none-any.whl", hash = "sha256:44c40f2cd7d6726af8fa6fe22549178d3a4dfecc35a9cd15ea916d9c83a688e0"}, + {file = "macholib-1.16.2.tar.gz", hash = "sha256:557bbfa1bb255c20e9abafe7ed6cd8046b48d9525db2f9b77d3122a63a2a8bf8"}, +] + +[package.dependencies] +altgraph = ">=0.17" + +[[package]] +name = "markdown" +version = "3.4.4" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, + {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pefile" +version = "2023.2.7" +description = "Python PE parsing module" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"}, + {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, +] + +[[package]] +name = "platformdirs" +version = "3.9.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"}, + {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"}, +] + +[package.extras] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.3.3" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pre_commit-3.3.3-py2.py3-none-any.whl", hash = "sha256:10badb65d6a38caff29703362271d7dca483d01da88f9d7e05d0b97171c136cb"}, + {file = "pre_commit-3.3.3.tar.gz", hash = "sha256:a2256f489cd913d575c145132ae196fe335da32d91a8294b7afe6622335dd023"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "progress" +version = "1.6" +description = "Easy to use progress bars" +optional = false +python-versions = "*" +files = [ + {file = "progress-1.6.tar.gz", hash = "sha256:c9c86e98b5c03fa1fe11e3b67c1feda4788b8d0fe7336c2ff7d5644ccfba34cd"}, +] + +[[package]] +name = "prometheus-client" +version = "0.17.1" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.6" +files = [ + {file = "prometheus_client-0.17.1-py3-none-any.whl", hash = "sha256:e537f37160f6807b8202a6fc4764cdd19bac5480ddd3e0d463c3002b34462101"}, + {file = "prometheus_client-0.17.1.tar.gz", hash = "sha256:21e674f39831ae3f8acde238afd9a27a37d0d2fb5a28ea094f0ce25d2cbf2091"}, +] + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "pycron" +version = "3.0.0" +description = "Simple cron-like parser, which determines if current datetime matches conditions." +optional = false +python-versions = ">=3.5" +files = [ + {file = "pycron-3.0.0.tar.gz", hash = "sha256:b916044e3e8253d5409c68df3ac64a3472c4e608dab92f40e8f595e5d3acb3de"}, +] + +[[package]] +name = "pyinstaller" +version = "5.13.0" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +optional = false +python-versions = "<3.13,>=3.7" +files = [ + {file = "pyinstaller-5.13.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:7fdd319828de679f9c5e381eff998ee9b4164bf4457e7fca56946701cf002c3f"}, + {file = "pyinstaller-5.13.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0df43697c4914285ecd333be968d2cd042ab9b2670124879ee87931d2344eaf5"}, + {file = "pyinstaller-5.13.0-py3-none-manylinux2014_i686.whl", hash = "sha256:28d9742c37e9fb518444b12f8c8ab3cb4ba212d752693c34475c08009aa21ccf"}, + {file = "pyinstaller-5.13.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e5fb17de6c325d3b2b4ceaeb55130ad7100a79096490e4c5b890224406fa42f4"}, + {file = "pyinstaller-5.13.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:78975043edeb628e23a73fb3ef0a273cda50e765f1716f75212ea3e91b09dede"}, + {file = "pyinstaller-5.13.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:cd7d5c06f2847195a23d72ede17c60857d6f495d6f0727dc6c9bc1235f2eb79c"}, + {file = "pyinstaller-5.13.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:24009eba63cfdbcde6d2634e9c87f545eb67249ddf3b514e0cd3b2cdaa595828"}, + {file = "pyinstaller-5.13.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:1fde4381155f21d6354dc450dcaa338cd8a40aaacf6bd22b987b0f3e1f96f3ee"}, + {file = "pyinstaller-5.13.0-py3-none-win32.whl", hash = "sha256:2d03419904d1c25c8968b0ad21da0e0f33d8d65716e29481b5bd83f7f342b0c5"}, + {file = "pyinstaller-5.13.0-py3-none-win_amd64.whl", hash = "sha256:9fc27c5a853b14a90d39c252707673c7a0efec921cd817169aff3af0fca8c127"}, + {file = "pyinstaller-5.13.0-py3-none-win_arm64.whl", hash = "sha256:3a331951f9744bc2379ea5d65d36f3c828eaefe2785f15039592cdc08560b262"}, + {file = "pyinstaller-5.13.0.tar.gz", hash = "sha256:5e446df41255e815017d96318e39f65a3eb807e74a796c7e7ff7f13b6366a2e9"}, +] + +[package.dependencies] +altgraph = "*" +macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} +pyinstaller-hooks-contrib = ">=2021.4" +pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} +setuptools = ">=42.0.0" + +[package.extras] +encryption = ["tinyaes (>=1.0.0)"] +hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2023.6" +description = "Community maintained hooks for PyInstaller" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyinstaller-hooks-contrib-2023.6.tar.gz", hash = "sha256:596a72009d8692b043e0acbf5e1b476d93149900142ba01845dded91a0770cb5"}, + {file = "pyinstaller_hooks_contrib-2023.6-py2.py3-none-any.whl", hash = "sha256:aa6d7d038814df6aa7bec7bdbebc7cb4c693d3398df858f6062957f0797d397b"}, +] + +[[package]] +name = "pytest" +version = "7.4.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-mock" +version = "3.11.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, + {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "python-pushsafer" +version = "1.1" +description = "Comprehensive bindings for the Pushsafer.com notification service" +optional = false +python-versions = "*" +files = [ + {file = "python-pushsafer-1.1.tar.gz", hash = "sha256:a0f92e9118c1a0d50f90968dbcfc67c80d034fc787155b893cff7316f0827864"}, + {file = "python_pushsafer-1.1-py3-none-any.whl", hash = "sha256:833362db904896d9b598b9da12385cf1f4673829bfc8efee3c928d83f88e6f12"}, +] + +[package.dependencies] +requests = ">=1.0" + +[[package]] +name = "python-telegram-bot" +version = "13.15" +description = "We have made you a wrapper you can't refuse" +optional = false +python-versions = ">=3.7" +files = [ + {file = "python-telegram-bot-13.15.tar.gz", hash = "sha256:b4047606b8081b62bbd6aa361f7ca1efe87fa8f1881ec9d932d35844bf57a154"}, + {file = "python_telegram_bot-13.15-py3-none-any.whl", hash = "sha256:06780c258d3f2a3c6c79a7aeb45714f4cd1dd6275941b7dc4628bba64fddd465"}, +] + +[package.dependencies] +APScheduler = "3.6.3" +cachetools = "4.2.2" +certifi = "*" +pytz = ">=2018.6" +tornado = "6.1" + +[package.extras] +json = ["ujson"] +passport = ["cryptography (!=3.4,!=3.4.1,!=3.4.2,!=3.4.3)"] +socks = ["PySocks"] + +[[package]] +name = "pytz" +version = "2023.3" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, + {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.2" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, + {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-oauthlib" +version = "1.3.1" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, + {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "responses" +version = "0.23.2" +description = "A utility library for mocking out the `requests` Python library." +optional = false +python-versions = ">=3.7" +files = [ + {file = "responses-0.23.2-py3-none-any.whl", hash = "sha256:9d49c218ba3079022bd63427f12b0a43b43d2f6aaf5ed859b9df9d733b4dd775"}, + {file = "responses-0.23.2.tar.gz", hash = "sha256:5d5a2ce3285f84e1f107d2e942476b6c7dff3747f289c0eae997cb77d2ab68e8"}, +] + +[package.dependencies] +pyyaml = "*" +requests = ">=2.30.0,<3.0" +types-PyYAML = "*" +urllib3 = ">=2.0.0,<3.0" + +[package.extras] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-requests"] + +[[package]] +name = "setuptools" +version = "68.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tornado" +version = "6.1" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">= 3.5" +files = [ + {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, + {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, + {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"}, + {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"}, + {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"}, + {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"}, + {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"}, + {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"}, + {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"}, + {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"}, + {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"}, + {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"}, + {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"}, + {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"}, + {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"}, + {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"}, + {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"}, + {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"}, + {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"}, + {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"}, + {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"}, + {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"}, + {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"}, + {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"}, + {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"}, + {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"}, + {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"}, + {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"}, + {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"}, + {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"}, + {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, + {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.11" +description = "Typing stubs for PyYAML" +optional = false +python-versions = "*" +files = [ + {file = "types-PyYAML-6.0.12.11.tar.gz", hash = "sha256:7d340b19ca28cddfdba438ee638cd4084bde213e501a3978738543e27094775b"}, + {file = "types_PyYAML-6.0.12.11-py3-none-any.whl", hash = "sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d"}, +] + +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "tzlocal" +version = "5.0.1" +description = "tzinfo object for the local timezone" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tzlocal-5.0.1-py3-none-any.whl", hash = "sha256:f3596e180296aaf2dbd97d124fe76ae3a0e3d32b258447de7b939b3fd4be992f"}, + {file = "tzlocal-5.0.1.tar.gz", hash = "sha256:46eb99ad4bdb71f3f72b7d24f4267753e240944ecfc16f25d2719ba89827a803"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + +[[package]] +name = "urllib3" +version = "2.0.4" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, + {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.24.2" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.24.2-py3-none-any.whl", hash = "sha256:43a3052be36080548bdee0b42919c88072037d50d56c28bd3f853cbe92b953ff"}, + {file = "virtualenv-20.24.2.tar.gz", hash = "sha256:fd8a78f46f6b99a67b7ec5cf73f92357891a7b3a40fd97637c27f854aae3b9e0"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<4" + +[package.extras] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "zipp" +version = "3.16.2" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, + {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.9,<3.13" +content-hash = "f71573229da7f84ac223f2e0907cdf10f037895a52beb3fb40d9903819e36a98" diff --git a/pyproject.toml b/pyproject.toml index 71009ac0..e9822a84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,54 @@ +[build-system] +build-backend = "poetry.core.masonry.api" +requires = ["poetry-core>=1.0.0"] + +[tool.poetry] +authors = ["Henning Merklinger "] +description = "Notifications for Too Good To Go" +documentation = "https://github.com/Der-Henning/tgtg/wiki" +keywords = ["tgtg", "toogoodtogo", "notifications"] +license = "GPL-3.0-or-later" +name = "tgtg-scanner" +packages = [{include = "tgtg_scanner"}] +readme = "README.md" +repository = "https://github.com/Der-Henning/tgtg" +version = "1.18.0_rc1" + +[tool.poetry.dependencies] +apprise = "^1.4.0" +colorlog = "^6.7.0" +cron-descriptor = "^1.4.0" +googlemaps = "^4.10.0" +humanize = "^4.7.0" +packaging = "^23.1" +progress = "^1.6" +prometheus-client = "^0.17.0" +pycron = "^3.0.0" +python = ">=3.9,<3.13" +python-pushsafer = "^1.1" +python-telegram-bot = "^13.15" +requests = "^2.31.0" + +[tool.poetry.group.build.dependencies] +pyinstaller = "^5.13.0" + +[tool.poetry.group.test.dependencies] +pre-commit = "^3.3.3" +pytest = "^7.4.0" +pytest-cov = "^4.1.0" +pytest-mock = "^3.11.1" +responses = "^0.23.1" + +[tool.poetry.scripts] +scanner = "tgtg_scanner.__main__:main" + [tool.pytest.ini_options] addopts = [ - "--import-mode=importlib", + "--import-mode=importlib" ] markers = [ - "tgtg_api: test directly calls the tgtg API (deselect with '-m \"not tgtg_api\"')" + "tgtg_api: test directly calls the tgtg API (deselect with '-m \"not tgtg_api\"')" ] pythonpath = [ - "src" + "." ] diff --git a/requirements-build.txt b/requirements-build.txt deleted file mode 100644 index 19d57136..00000000 --- a/requirements-build.txt +++ /dev/null @@ -1,4 +0,0 @@ --r requirements.txt -altgraph==0.17.3 -pyinstaller==5.12.0 -pyinstaller-hooks-contrib==2023.3 diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 2d3468a7..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,9 +0,0 @@ --r requirements-build.txt -autopep8 -flake8 -ipykernel -pre-commit -pytest -pytest-cov -pytest-mock -responses diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 9a49f1aa..00000000 --- a/requirements.txt +++ /dev/null @@ -1,24 +0,0 @@ -apprise==1.3.0 -APScheduler==3.6.3 -cachetools==4.2.2 -certifi==2023.5.7 -charset-normalizer==3.1.0 -colorlog==6.7.0 -cron-descriptor==1.4.0 -googlemaps==4.10.0 -humanize==4.6.0 -idna==3.4 -packaging==23.1 -progress==1.6 -prometheus-client==0.17.0 -pycron==3.0.0 -python-pushsafer==1.1 -python-telegram-bot==13.15 -pytz==2023.3 -pytz-deprecation-shim==0.1.0.post0 -requests==2.31.0 -six==1.16.0 -tornado==6.1 -tzdata==2023.3 -tzlocal==5.0.1 -urllib3==2.0.2 diff --git a/sample.env b/sample.env deleted file mode 100644 index 4146bfcd..00000000 --- a/sample.env +++ /dev/null @@ -1,83 +0,0 @@ -TZ=Europe/Berlin -DEBUG=false -SLEEP_TIME=60 -SCHEDULE_CRON= -ITEM_IDS= -METRICS=false -METRICS_PORT=8000 -DISABLE_TESTS=false -QUIET=false -LOCALE=en_US - -TGTG_USERNAME= -TGTG_ACCESS_TOKEN= -TGTG_REFRESH_TOKEN= -TGTG_USER_ID= -TGTG_DATADOME= - -APPRISE=false -APPRISE_URL= -APPRISE_TITLE= -APPRISE_BODY= -APPRISE_CRON= - -CONSOLE=false -CONSOLE_BODY= -CONSOLE_CRON= - -SMTP=false -SMTP_HOST=smtp.gmail.com -SMTP_PORT=587 -SMTP_USERNAME= -SMTP_PASSWORD= -SMTP_TLS=true -SMTP_SSL=false -SMTP_SENDER= -SMTP_RECIPIENT= -SMTP_CRON= - -PUSH_SAFER=false -PUSH_SAFER_KEY= -PUSH_SAFER_DEVICE_ID= -PUSH_SAFER_CRON= - -IFTTT=false -IFTTT_EVENT=tgtg_notification -IFTTT_BODY= -IFTTT_KEY= -IFTTT_CRON= - -NTFY=false -NTFY_SERVER=https://ntfy.sh -NTFY_TOPIC=tgtg_notification -NTFY_TITLE= -NTFY_BODY= -NTFY_PRIORITY= -NTFY_TAGS=tgtg -NTFY_CLICK=https://share.toogoodtogo.com/item/${{item_id}} -NTFY_USERNAME= -NTFY_PASSWORD= -NTFY_TIMEOUT=60 -NTFY_CRON= - -TELEGRAM=false -TELEGRAM_TOKEN= -TELEGRAM_CHAT_IDS= -TELEGRAM_TIMEOUT=60 -TELEGRAM_BODY= -TELEGRAM_CRON= - -WEBHOOK=false -WEBHOOK_URL= -WEBHOOK_METHOD=POST -WEBHOOK_BODY= -WEBHOOK_TYPE= -WEBHOOK_USERNAME= -WEBHOOK_PASSWORD= -WEBHOOK_TIMEOUT=60 -WEBHOOK_CRON= - -SCRIPT=false -SCRIPT_COMMAND= -SCRIPT_TIMEOUT=60 -SCRIPT_CRON= diff --git a/scanner.spec b/scanner.spec index b11524cb..a69b3572 100644 --- a/scanner.spec +++ b/scanner.spec @@ -1,5 +1,5 @@ # -*- mode: python ; coding: utf-8 -*- -from PyInstaller.utils.hooks import collect_all +from PyInstaller.utils.hooks import collect_all, copy_metadata datas = [] binaries = [] @@ -10,13 +10,13 @@ tmp_ret = collect_all('humanize') datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] tmp_ret = collect_all('cron_descriptor') datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] - +datas += copy_metadata('tgtg_scanner') block_cipher = None a = Analysis( - ['src/main.py'], + ['tgtg_scanner/__main__.py'], pathex=[], binaries=binaries, datas=datas, diff --git a/src/_version.py b/src/_version.py deleted file mode 100644 index bac32d4d..00000000 --- a/src/_version.py +++ /dev/null @@ -1,7 +0,0 @@ -__title__ = "TGTG Scanner" -__description__ = "Provides notifications for TGTG magic bags" -__version__ = "1.17.1" -__author__ = "Henning Merklinger" -__author_email__ = "henning.merklinger@gmail.com" -__license__ = "GPL" -__url__ = "https://github.com/Der-Henning/tgtg" diff --git a/src/models/__init__.py b/src/models/__init__.py deleted file mode 100644 index eda829b3..00000000 --- a/src/models/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from models.config import Config -from models.cron import Cron -from models.favorites import Favorites -from models.item import Item -from models.location import Location -from models.metrics import Metrics -from models.reservations import Reservations - -__all__ = ['Config', 'Cron', 'Favorites', 'Item', - 'Metrics', 'Location', 'Reservations'] diff --git a/src/notifiers/__init__.py b/src/notifiers/__init__.py deleted file mode 100644 index 2aa227ed..00000000 --- a/src/notifiers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from notifiers.base import Notifier -from notifiers.notifiers import Notifiers - -__all__ = ['Notifier', 'Notifiers'] diff --git a/src/tgtg/__init__.py b/src/tgtg/__init__.py deleted file mode 100644 index 26f5890a..00000000 --- a/src/tgtg/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from tgtg.tgtg_client import TgtgClient - -__all__ = ['TgtgClient'] diff --git a/tests/conftest.py b/tests/conftest.py index 86a2211f..d8b0f145 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import pytest -from models import Item +from tgtg_scanner.models import Item @pytest.fixture @@ -162,7 +162,7 @@ def tgtg_item(): 'is_automatically_created': False }, 'is_manufacturer': False}, - 'display_name': 'Chutney Indian Food - Hamburg – Europapassage 2.OG', + 'display_name': 'Chutney Indian Food - Hamburg - Europapassage 2.OG', 'pickup_interval': { 'start': '2022-12-30T19:00:00Z', 'end': '2022-12-30T19:30:00Z' diff --git a/tests/test_config.py b/tests/test_config.py index 6e6f05c0..6acf7a71 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,31 +4,33 @@ import pytest -import models.config -from models.cron import Cron +import tgtg_scanner.models.config +from tgtg_scanner.models.cron import Cron def test_default_ini_config(): - reload(models.config) - config = models.config.Config("") - for key in models.config.DEFAULT_CONFIG: + reload(tgtg_scanner.models.config) + config = tgtg_scanner.models.config.Config("") + for key in tgtg_scanner.models.config.DEFAULT_CONFIG: assert hasattr(config, key) - assert getattr(config, key) == models.config.DEFAULT_CONFIG.get(key) + assert getattr( + config, key) == tgtg_scanner.models.config.DEFAULT_CONFIG.get(key) def test_default_env_config(): - reload(models.config) - config = models.config.Config() - for key in models.config.DEFAULT_CONFIG: + reload(tgtg_scanner.models.config) + config = tgtg_scanner.models.config.Config() + for key in tgtg_scanner.models.config.DEFAULT_CONFIG: assert hasattr(config, key) - assert getattr(config, key) == models.config.DEFAULT_CONFIG.get(key) + assert getattr( + config, key) == tgtg_scanner.models.config.DEFAULT_CONFIG.get(key) def test_config_set(temp_path: Path): - reload(models.config) + reload(tgtg_scanner.models.config) config_path = Path(temp_path, "config.ini") config_path.touch(exist_ok=True) - config = models.config.Config(config_path.absolute()) + config = tgtg_scanner.models.config.Config(config_path.absolute()) assert config.set("MAIN", "debug", True) @@ -39,10 +41,10 @@ def test_config_set(temp_path: Path): def test_save_tokens_to_ini(temp_path: Path): - reload(models.config) + reload(tgtg_scanner.models.config) config_path = Path(temp_path, "config.ini") config_path.touch(exist_ok=True) - config = models.config.Config(config_path.absolute()) + config = tgtg_scanner.models.config.Config(config_path.absolute()) config.save_tokens("test_access_token", "test_refresh_token", "test_user_id", "test_cookie") @@ -56,10 +58,10 @@ def test_save_tokens_to_ini(temp_path: Path): def test_token_path(temp_path: Path, monkeypatch: pytest.MonkeyPatch): - reload(models.config) + reload(tgtg_scanner.models.config) monkeypatch.setenv("TGTG_TOKEN_PATH", str(temp_path.absolute())) - config = models.config.Config() + config = tgtg_scanner.models.config.Config() config.save_tokens("test_access_token", "test_refresh_token", "test_user_id", "test_cookie") config._load_tokens() @@ -71,7 +73,7 @@ def test_token_path(temp_path: Path, monkeypatch: pytest.MonkeyPatch): def test_ini_get(temp_path: Path): - reload(models.config) + reload(tgtg_scanner.models.config) config_path = Path(temp_path, "config.ini") with open(config_path, 'w', encoding='utf-8') as file: @@ -87,7 +89,7 @@ def test_ini_get(temp_path: Path): '${{price}} € \\nÀ récupérer"}' ]) - config = models.config.Config(config_path.absolute()) + config = tgtg_scanner.models.config.Config(config_path.absolute()) assert config.debug is True assert config.item_ids == ["23423", "32432", "234532"] @@ -100,7 +102,7 @@ def test_ini_get(temp_path: Path): def test_env_get(monkeypatch: pytest.MonkeyPatch): - reload(models.config) + reload(tgtg_scanner.models.config) monkeypatch.setenv("DEBUG", "true") monkeypatch.setenv("ITEM_IDS", "23423, 32432, 234532") monkeypatch.setenv("WEBHOOK_TIMEOUT", "42") @@ -109,7 +111,7 @@ def test_env_get(monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("WEBHOOK_BODY", '{"content": "${{items_available}} ' 'panier(s) à ${{price}} € \\nÀ récupérer"}') - config = models.config.Config() + config = tgtg_scanner.models.config.Config() assert config.debug is True assert config.item_ids == ["23423", "32432", "234532"] diff --git a/tests/test_cron.py b/tests/test_cron.py index 7efd5174..e8a9551c 100644 --- a/tests/test_cron.py +++ b/tests/test_cron.py @@ -1,8 +1,8 @@ import pytest -from models.cron import Cron -from models.errors import ConfigurationError +from tgtg_scanner.models.cron import Cron +from tgtg_scanner.models.errors import ConfigurationError def test_description(): diff --git a/tests/test_favorites.py b/tests/test_favorites.py index 6fc459c9..b4ae8fb2 100644 --- a/tests/test_favorites.py +++ b/tests/test_favorites.py @@ -2,8 +2,8 @@ import pytest -from models.errors import TgtgAPIError -from models.favorites import Favorites +from tgtg_scanner.models.errors import TgtgAPIError +from tgtg_scanner.models.favorites import Favorites @pytest.fixture @@ -15,8 +15,7 @@ def favorites(): def test_is_item_favorite(favorites: Favorites, tgtg_item: dict): favorites.client.get_favorites.return_value = [] is_favorite = favorites.is_item_favorite( - tgtg_item.get("item", {}).get("item_id") - ) + tgtg_item.get("item", {}).get("item_id")) assert is_favorite == tgtg_item.get("favorite") is_favorite = favorites.is_item_favorite("123") @@ -59,5 +58,4 @@ def test_remove_favorites(favorites: Favorites): favorites.client.set_favorite = set_favorite_mock favorites.remove_favorite(["123", "234"]) set_favorite_mock.assert_has_calls( - [call("123", False), call("234", False)] - ) + [call("123", False), call("234", False)]) diff --git a/tests/test_location.py b/tests/test_location.py index 84c04a28..7470a626 100644 --- a/tests/test_location.py +++ b/tests/test_location.py @@ -1,6 +1,6 @@ from pytest_mock.plugin import MockerFixture -from models import Location +from tgtg_scanner.models import Location def test_calculate_distance_time(mocker: MockerFixture): diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 015948fb..b2ba4c4a 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -1,6 +1,6 @@ import requests -from models.metrics import Metrics +from tgtg_scanner.models.metrics import Metrics def test_metrics(): diff --git a/tests/test_notifiers.py b/tests/test_notifiers.py index a8f640b5..df688047 100644 --- a/tests/test_notifiers.py +++ b/tests/test_notifiers.py @@ -6,14 +6,14 @@ import pytest import responses -import models.config -from models.item import Item -from notifiers.apprise import Apprise -from notifiers.console import Console -from notifiers.ifttt import IFTTT -from notifiers.ntfy import Ntfy -from notifiers.script import Script -from notifiers.webhook import WebHook +import tgtg_scanner.models.config +from tgtg_scanner.models.item import Item +from tgtg_scanner.notifiers.apprise import Apprise +from tgtg_scanner.notifiers.console import Console +from tgtg_scanner.notifiers.ifttt import IFTTT +from tgtg_scanner.notifiers.ntfy import Ntfy +from tgtg_scanner.notifiers.script import Script +from tgtg_scanner.notifiers.webhook import WebHook SYS_PLATFORM = platform.system() IS_WINDOWS = SYS_PLATFORM.lower() in ('windows', 'cygwin') @@ -21,8 +21,8 @@ @responses.activate def test_webhook_json(test_item: Item): - reload(models.config) - config = models.config.Config("") + reload(tgtg_scanner.models.config) + config = tgtg_scanner.models.config.Config("") config._setattr("webhook.enabled", True) config._setattr("webhook.method", "POST") config._setattr("webhook.url", "https://api.example.com") @@ -58,8 +58,8 @@ def test_webhook_json(test_item: Item): @responses.activate def test_webhook_text(test_item: Item): - reload(models.config) - config = models.config.Config("") + reload(tgtg_scanner.models.config) + config = tgtg_scanner.models.config.Config("") config._setattr("webhook.enabled", True) config._setattr("webhook.method", "POST") config._setattr("webhook.url", "https://api.example.com") @@ -91,8 +91,8 @@ def test_webhook_text(test_item: Item): @responses.activate def test_ifttt(test_item: Item): - reload(models.config) - config = models.config.Config("") + reload(tgtg_scanner.models.config) + config = tgtg_scanner.models.config.Config("") config._setattr("ifttt.enabled", True) config._setattr("ifttt.event", "tgtg_notification") config._setattr("ifttt.key", "secret_key") @@ -126,8 +126,8 @@ def test_ifttt(test_item: Item): @responses.activate def test_ntfy(test_item: Item): - reload(models.config) - config = models.config.Config("") + reload(tgtg_scanner.models.config) + config = tgtg_scanner.models.config.Config("") config._setattr("ntfy.enabled", True) config._setattr("ntfy.server", "https://ntfy.sh") config._setattr("ntfy.topic", "tgtg_test") @@ -158,8 +158,8 @@ def test_ntfy(test_item: Item): @responses.activate def test_apprise(test_item: Item): - reload(models.config) - config = models.config.Config("") + reload(tgtg_scanner.models.config) + config = tgtg_scanner.models.config.Config("") config._setattr("apprise.enabled", True) config._setattr("apprise.url", "ntfy://tgtg_test") config._setattr("apprise.title", "New Items - ${{display_name}}") @@ -186,8 +186,8 @@ def test_apprise(test_item: Item): def test_console(test_item: Item, capsys: pytest.CaptureFixture): - reload(models.config) - config = models.config.Config("") + reload(tgtg_scanner.models.config) + config = tgtg_scanner.models.config.Config("") config._setattr("console.enabled", True) config._setattr("console.body", "${{display_name}} - " "new amount: ${{items_available}}") @@ -202,8 +202,8 @@ def test_console(test_item: Item, capsys: pytest.CaptureFixture): def test_script(test_item: Item, capfdbinary: pytest.CaptureFixture): - reload(models.config) - config = models.config.Config("") + reload(tgtg_scanner.models.config) + config = tgtg_scanner.models.config.Config("") config._setattr("script.enabled", True) config._setattr("script.command", "echo ${{display_name}}") diff --git a/tests/test_reservations.py b/tests/test_reservations.py index d57041cf..3fc8fc74 100644 --- a/tests/test_reservations.py +++ b/tests/test_reservations.py @@ -2,14 +2,13 @@ import pytest -from models.item import Item -from models.reservations import Order, Reservation, Reservations +from tgtg_scanner.models.item import Item +from tgtg_scanner.models.reservations import Order, Reservation, Reservations -@pytest.fixture +@pytest.fixture(scope="function") def reservations(): mock_client = MagicMock() - mock_client.get_order_status.return_value = {"state": "RESERVED"} return Reservations(mock_client) @@ -25,13 +24,13 @@ def test_make_orders(reservations: Reservations, tgtg_item: dict): assert len(reservations.active_orders) == 1 assert len(reservations.reservation_query) == 0 callback_mock.assert_called_once_with( - Reservation("123", 1, "Test Item") - ) + Reservation("123", 1, "Test Item")) def test_update_active_orders(reservations: Reservations): order = Order("1", "123", 1, "Test Item") - reservations.active_orders = [order] + reservations.client.get_order_status.return_value = {"state": "RESERVED"} + reservations.active_orders = {order.id: order} reservations.update_active_orders() assert len(reservations.active_orders) == 1 reservations.client.get_order_status.return_value = {"state": "CANELLED"} @@ -50,24 +49,3 @@ def test_cancel_all_orders(reservations: Reservations): order2 = Order("2", "123", 2, "Test Item 2") reservations.active_orders = [order1, order2] reservations.cancel_all_orders() - - -def test_get_favorites(reservations: Reservations, tgtg_item: dict): - reservations.client.get_favorites.return_value = [tgtg_item] - favorites = reservations.get_favorites() - assert len(favorites) == 1 - assert favorites[0].item_id == tgtg_item.get("item", {}).get("item_id") - assert favorites[0].display_name == tgtg_item.get("display_name") - assert favorites[0].items_available == tgtg_item.get("items_available") - - -def test_create_order(reservations: Reservations): - reservations.client.create_order.return_value = {"id": "1"} - reservation = Reservation("123", 1, "Test Item") - reservations._create_order(reservation) - assert len(reservations.active_orders) == 1 - assert len(reservations.reservation_query) == 0 - assert reservations.active_orders[0].id == "1" - assert reservations.active_orders[0].item_id == "123" - assert reservations.active_orders[0].amount == 1 - assert reservations.active_orders[0].display_name == "Test Item" diff --git a/tests/test_tgtg.py b/tests/test_tgtg.py index 8fbf9562..b966cc24 100644 --- a/tests/test_tgtg.py +++ b/tests/test_tgtg.py @@ -8,8 +8,8 @@ import responses from pytest_mock.plugin import MockerFixture -import models.config -from tgtg.tgtg_client import USER_AGENTS, TgtgClient +import tgtg_scanner.models.config +from tgtg_scanner.tgtg.tgtg_client import USER_AGENTS, TgtgClient def test_get_latest_apk_version(): @@ -20,8 +20,9 @@ def test_get_latest_apk_version(): def test_get_user_agent(mocker: MockerFixture): apk_version = "22.11.11" - mocker.patch('tgtg.tgtg_client.TgtgClient.get_latest_apk_version', - return_value=apk_version) + mocker.patch( + 'tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version', + return_value=apk_version) client = TgtgClient( email='test@example.com' ) @@ -31,8 +32,9 @@ def test_get_user_agent(mocker: MockerFixture): @responses.activate def test_tgtg_login_with_mail(mocker: MockerFixture): - mocker.patch('tgtg.tgtg_client.TgtgClient.get_latest_apk_version', - return_value="22.11.11") + mocker.patch( + 'tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version', + return_value="22.11.11") client = TgtgClient( email='test@example.com', polling_wait_time=1 @@ -77,8 +79,9 @@ def test_tgtg_login_with_mail(mocker: MockerFixture): @responses.activate def test_tgtg_login_with_token(mocker: MockerFixture): - mocker.patch('tgtg.tgtg_client.TgtgClient.get_latest_apk_version', - return_value="22.11.11") + mocker.patch( + 'tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version', + return_value="22.11.11") client = TgtgClient( email='test@example.com', access_token='old_access_token', @@ -103,9 +106,11 @@ def test_tgtg_login_with_token(mocker: MockerFixture): @responses.activate def test_tgtg_get_items(mocker: MockerFixture, tgtg_item: dict): - mocker.patch('tgtg.tgtg_client.TgtgClient.get_latest_apk_version', - return_value="22.11.11") - mocker.patch('tgtg.tgtg_client.TgtgClient.login', return_value=None) + mocker.patch( + 'tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version', + return_value="22.11.11") + mocker.patch('tgtg_scanner.tgtg.tgtg_client.TgtgClient.login', + return_value=None) responses.add( responses.POST, "https://apptoogoodtogo.com/api/item/v8/", @@ -124,9 +129,11 @@ def test_tgtg_get_items(mocker: MockerFixture, tgtg_item: dict): @responses.activate def test_tgtg_get_item(mocker: MockerFixture, tgtg_item: dict): - mocker.patch('tgtg.tgtg_client.TgtgClient.get_latest_apk_version', - return_value="22.11.11") - mocker.patch('tgtg.tgtg_client.TgtgClient.login', return_value=None) + mocker.patch( + 'tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version', + return_value="22.11.11") + mocker.patch('tgtg_scanner.tgtg.tgtg_client.TgtgClient.login', + return_value=None) item_id = tgtg_item.get('item', {}).get('item_id') responses.add( responses.POST, @@ -146,9 +153,11 @@ def test_tgtg_get_item(mocker: MockerFixture, tgtg_item: dict): @responses.activate def test_tgtg_set_favorite(mocker: MockerFixture): - mocker.patch('tgtg.tgtg_client.TgtgClient.get_latest_apk_version', - return_value="22.11.11") - mocker.patch('tgtg.tgtg_client.TgtgClient.login', return_value=None) + mocker.patch( + 'tgtg_scanner.tgtg.tgtg_client.TgtgClient.get_latest_apk_version', + return_value="22.11.11") + mocker.patch('tgtg_scanner.tgtg.tgtg_client.TgtgClient.login', + return_value=None) item_id = "12345" responses.add( responses.POST, @@ -168,11 +177,11 @@ def test_tgtg_set_favorite(mocker: MockerFixture): @pytest.mark.tgtg_api def test_tgtg_api(item_properties: dict): - reload(models.config) - if pathlib.Path('src/config.ini').exists(): - config = models.config.Config('src/config.ini') + reload(tgtg_scanner.models.config) + if pathlib.Path('config.ini').is_file(): + config = tgtg_scanner.models.config.Config('config.ini') else: - config = models.config.Config() + config = tgtg_scanner.models.config.Config() env_file = environ.get("GITHUB_ENV", None) diff --git a/tgtg_scanner/__init__.py b/tgtg_scanner/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/main.py b/tgtg_scanner/__main__.py similarity index 83% rename from src/main.py rename to tgtg_scanner/__main__.py index 469c57a9..47a23ccb 100644 --- a/src/main.py +++ b/tgtg_scanner/__main__.py @@ -14,10 +14,11 @@ from packaging import version from requests.exceptions import RequestException -from _version import __author__, __url__, __version__ -from models import Config -from models.errors import ConfigurationError, TgtgAPIError -from scanner import Scanner +from tgtg_scanner._version import (__author__, __description__, __url__, + __version__) +from tgtg_scanner.models import Config +from tgtg_scanner.models.errors import ConfigurationError, TgtgAPIError +from tgtg_scanner.scanner import Scanner VERSION_URL = "https://api.github.com/repos/Der-Henning/tgtg/releases/latest" @@ -34,75 +35,74 @@ SYS_PLATFORM = platform.system() IS_WINDOWS = SYS_PLATFORM.lower() in ('windows', 'cygwin') IS_EXECUTABLE = getattr(sys, "_MEIPASS", False) -PROG_PATH = Path(sys.executable) if IS_EXECUTABLE else Path(__file__) +PROG_PATH = Path(sys.executable).parent if IS_EXECUTABLE else Path(os.getcwd()) IS_DOCKER = os.environ.get("DOCKER", "False").lower() in ('true', '1', 't') +LOGS_PATH = os.environ.get("LOGS_PATH", PROG_PATH) def main() -> NoReturn: """Wrapper for Scanner and Helper functions.""" _register_signals() - config_file = Path(PROG_PATH.parent, "config.ini") - log_file = Path(PROG_PATH.parent, "scanner.log") + config_file = _get_config_file() + log_file = Path(LOGS_PATH, "scanner.log") - parser = argparse.ArgumentParser( - description="TooGoodToGo scanner and notifier.", - prog=PROG_PATH.name - ) + parser = argparse.ArgumentParser(description=__description__) parser.add_argument( "-v", "--version", action="version", - version=f"v{__version__}" - ) + version=f"v{__version__}") parser.add_argument( "-d", "--debug", action="store_true", - help="activate debugging mode" - ) + help="activate debugging mode") + parser.add_argument( + "-c", "--config", + metavar="config_file", + type=Path, + help="path to config file (default: config.ini)") + parser.add_argument( + "-l", "--log_file", + metavar="log_file", + type=Path, + default=log_file, + help="path to log file (default: scanner.log)") helper_group = parser.add_mutually_exclusive_group(required=False) helper_group.add_argument( "-t", "--tokens", action="store_true", - help="display your current access tokens and exit", - ) + help="display your current access tokens and exit",) helper_group.add_argument( "-f", "--favorites", action="store_true", - help="display your favorites and exit" - ) + help="display your favorites and exit") helper_group.add_argument( "-F", "--favorite_ids", action="store_true", - help="display the item ids of your favorites and exit", - ) + help="display the item ids of your favorites and exit",) helper_group.add_argument( "-a", "--add", nargs="+", metavar="item_id", - help="add item ids to favorites and exit", - ) + help="add item ids to favorites and exit",) helper_group.add_argument( "-r", "--remove", nargs="+", metavar="item_id", - help="remove item ids from favorites and exit", - ) + help="remove item ids from favorites and exit",) helper_group.add_argument( "-R", "--remove_all", action="store_true", - help="remove all favorites and exit" - ) + help="remove all favorites and exit") json_group = parser.add_mutually_exclusive_group(required=False) json_group.add_argument( "-j", "--json", action="store_true", - help="output as plain json" - ) + help="output as plain json") json_group.add_argument( "-J", "--json_pretty", action="store_true", - help="output as pretty json" - ) + help="output as pretty json") args = parser.parse_args() # Disable logging for json output @@ -128,15 +128,14 @@ def main() -> NoReturn: "INFO": "green", "WARNING": "yellow", "ERROR": "red", - "CRITICAL": "red", - }, - ) + "CRITICAL": "red"}) stream_handler = logging.StreamHandler() stream_handler.setFormatter(stream_formatter) logging.root.addHandler(stream_handler) # Define file formatter and handler - file_handler = logging.FileHandler(log_file, mode="w", encoding='utf-8') + file_handler = logging.FileHandler( + args.log_file, mode="w", encoding='utf-8') file_formatter = logging.Formatter( fmt=("[%(asctime)s][%(name)s]" "[%(filename)s:%(funcName)s:%(lineno)d]" @@ -149,10 +148,16 @@ def main() -> NoReturn: log = logging.getLogger("tgtg") log.setLevel(logging.INFO) + # Set config file from args + if args.config: + if not args.config.is_file(): + log.error("Config file %s not found!", args.config) + sys.exit(1) + config_file = args.config + try: # Load config - config = (Config(config_file) if Path(config_file).is_file() - else Config()) + config = Config(config_file) config.docker = IS_DOCKER # Activate debugging mode @@ -231,6 +236,15 @@ def main() -> NoReturn: sys.exit(1) +def _get_config_file() -> Union[Path, None]: + config_file = Path(PROG_PATH, "config.ini") + if config_file.is_file(): + return config_file + config_file = Path(PROG_PATH, "tgtg_scanner", "config.ini") + if config_file.is_file(): + return config_file + + def _get_version_info() -> str: lastest_release = _get_new_version() if lastest_release is None: diff --git a/tgtg_scanner/_version.py b/tgtg_scanner/_version.py new file mode 100644 index 00000000..0508e4b2 --- /dev/null +++ b/tgtg_scanner/_version.py @@ -0,0 +1,13 @@ +import importlib.metadata + +PAKAGE_NAME = "tgtg_scanner" + +metadata = importlib.metadata.metadata(PAKAGE_NAME) + +__title__ = metadata.get("Name") +__description__ = metadata.get("Summary") +__version__ = importlib.metadata.version(PAKAGE_NAME) +__author__ = metadata.get("Author") +__author_email__ = metadata.get("Author-email") +__license__ = metadata.get("License") +__url__ = metadata.get("Project-URL").split(", ")[1] diff --git a/tgtg_scanner/models/__init__.py b/tgtg_scanner/models/__init__.py new file mode 100644 index 00000000..398afe74 --- /dev/null +++ b/tgtg_scanner/models/__init__.py @@ -0,0 +1,10 @@ +from tgtg_scanner.models.config import Config +from tgtg_scanner.models.cron import Cron +from tgtg_scanner.models.favorites import Favorites +from tgtg_scanner.models.item import Item +from tgtg_scanner.models.location import Location +from tgtg_scanner.models.metrics import Metrics +from tgtg_scanner.models.reservations import Reservations + +__all__ = ['Config', 'Cron', 'Item', 'Metrics', + 'Location', 'Reservations', 'Favorites'] diff --git a/src/models/config.py b/tgtg_scanner/models/config.py similarity index 99% rename from src/models/config.py rename to tgtg_scanner/models/config.py index 10e67037..82641075 100644 --- a/src/models/config.py +++ b/tgtg_scanner/models/config.py @@ -9,8 +9,8 @@ import humanize -from models.cron import Cron -from models.errors import ConfigurationError +from tgtg_scanner.models.cron import Cron +from tgtg_scanner.models.errors import ConfigurationError log = logging.getLogger("tgtg") diff --git a/src/models/cron.py b/tgtg_scanner/models/cron.py similarity index 96% rename from src/models/cron.py rename to tgtg_scanner/models/cron.py index a9d04b6f..7717a5f4 100644 --- a/src/models/cron.py +++ b/tgtg_scanner/models/cron.py @@ -3,7 +3,7 @@ import pycron from cron_descriptor import Options, get_description -from models.errors import ConfigurationError +from tgtg_scanner.models.errors import ConfigurationError log = logging.getLogger('tgtg') diff --git a/src/models/errors.py b/tgtg_scanner/models/errors.py similarity index 100% rename from src/models/errors.py rename to tgtg_scanner/models/errors.py diff --git a/src/models/favorites.py b/tgtg_scanner/models/favorites.py similarity index 93% rename from src/models/favorites.py rename to tgtg_scanner/models/favorites.py index cda7990c..1ecf55f9 100644 --- a/src/models/favorites.py +++ b/tgtg_scanner/models/favorites.py @@ -2,9 +2,9 @@ from dataclasses import dataclass from typing import List -from models.errors import TgtgAPIError -from models.item import Item -from tgtg import TgtgClient +from tgtg_scanner.models.errors import TgtgAPIError +from tgtg_scanner.models.item import Item +from tgtg_scanner.tgtg import TgtgClient log = logging.getLogger("tgtg") diff --git a/src/models/item.py b/tgtg_scanner/models/item.py similarity index 98% rename from src/models/item.py rename to tgtg_scanner/models/item.py index 699b0282..6e3db200 100644 --- a/src/models/item.py +++ b/tgtg_scanner/models/item.py @@ -7,8 +7,8 @@ import humanize import requests -from models.errors import MaskConfigurationError -from models.location import DistanceTime, Location +from tgtg_scanner.models.errors import MaskConfigurationError +from tgtg_scanner.models.location import DistanceTime, Location ATTRS = ["item_id", "items_available", "display_name", "description", "price", "currency", "pickupdate", "favorite", "rating", diff --git a/src/models/location.py b/tgtg_scanner/models/location.py similarity index 97% rename from src/models/location.py rename to tgtg_scanner/models/location.py index 2805e598..ccd8f0a3 100644 --- a/src/models/location.py +++ b/tgtg_scanner/models/location.py @@ -4,7 +4,7 @@ import googlemaps -from models.errors import LocationConfigurationError +from tgtg_scanner.models.errors import LocationConfigurationError log = logging.getLogger("tgtg") diff --git a/src/models/metrics.py b/tgtg_scanner/models/metrics.py similarity index 100% rename from src/models/metrics.py rename to tgtg_scanner/models/metrics.py diff --git a/src/models/reservations.py b/tgtg_scanner/models/reservations.py similarity index 64% rename from src/models/reservations.py rename to tgtg_scanner/models/reservations.py index 5ff24e3d..9034f5f5 100644 --- a/src/models/reservations.py +++ b/tgtg_scanner/models/reservations.py @@ -2,8 +2,8 @@ from dataclasses import dataclass from typing import Callable, Dict, List -from models.item import Item -from tgtg import TgtgClient +from tgtg_scanner.models.item import Item +from tgtg_scanner.tgtg import TgtgClient log = logging.getLogger("tgtg") @@ -27,7 +27,7 @@ class Reservations(): def __init__(self, client: TgtgClient) -> None: self.client = client self.reservation_query: List[Reservation] = [] - self.active_orders: List[Order] = [] + self.active_orders: Dict[str, Order] = {} def reserve(self, item_id: str, display_name: str, @@ -52,43 +52,39 @@ def make_orders(self, state: Dict[str, Item], """ for reservation in self.reservation_query: if state.get(reservation.item_id).items_available > 0: - self._create_order(reservation) - callback(reservation) + try: + self._create_order(reservation) + self.reservation_query.remove(reservation) + callback(reservation) + except Exception as exc: + log.error("Create Order Error: %s", exc) def update_active_orders(self) -> None: """Remove orders that are not active anymore """ - for order in self.active_orders: - res = self.client.get_order_status(order.id) + for order_id in list(self.active_orders): + res = self.client.get_order_status(order_id) if res.get("state") != "RESERVED": - self.active_orders.remove(order) + del self.active_orders[order_id] - def cancel_order(self, order: Order) -> None: + def cancel_order(self, order_id: str) -> None: """Cancel an order """ - self.client.abort_order(order.id) + self.client.abort_order(order_id) def cancel_all_orders(self) -> None: """Cancel all active orders """ - for order in self.active_orders: - self.cancel_order(order) - - def get_favorites(self) -> List[Item]: - """Get all favorite items - """ - return [Item(item) for item in self.client.get_favorites()] + for order_id in list(self.active_orders): + self.cancel_order(order_id) def _create_order(self, reservation: Reservation) -> None: - try: - res = self.client.create_order( - reservation.item_id, reservation.amount) - order_id = res.get("id") + res = self.client.create_order( + reservation.item_id, reservation.amount) + order_id = res.get("id") + if order_id: order = Order(order_id, reservation.item_id, reservation.amount, reservation.display_name) - self.active_orders.append(order) - self.reservation_query.remove(reservation) - except Exception as exc: - log.error("Create Order Error: %s", exc) + self.active_orders[order_id] = order diff --git a/tgtg_scanner/notifiers/__init__.py b/tgtg_scanner/notifiers/__init__.py new file mode 100644 index 00000000..66f6e696 --- /dev/null +++ b/tgtg_scanner/notifiers/__init__.py @@ -0,0 +1,4 @@ +from tgtg_scanner.notifiers.base import Notifier +from tgtg_scanner.notifiers.notifiers import Notifiers + +__all__ = ['Notifier', 'Notifiers'] diff --git a/src/notifiers/apprise.py b/tgtg_scanner/notifiers/apprise.py similarity index 86% rename from src/notifiers/apprise.py rename to tgtg_scanner/notifiers/apprise.py index ac41e9fa..c09a3a9e 100644 --- a/src/notifiers/apprise.py +++ b/tgtg_scanner/notifiers/apprise.py @@ -2,9 +2,10 @@ import apprise -from models import Config, Item -from models.errors import AppriseConfigurationError, MaskConfigurationError -from notifiers.base import Notifier +from tgtg_scanner.models import Config, Item +from tgtg_scanner.models.errors import (AppriseConfigurationError, + MaskConfigurationError) +from tgtg_scanner.notifiers.base import Notifier log = logging.getLogger('tgtg') diff --git a/src/notifiers/base.py b/tgtg_scanner/notifiers/base.py similarity index 91% rename from src/notifiers/base.py rename to tgtg_scanner/notifiers/base.py index b2af6ab5..7801efba 100644 --- a/src/notifiers/base.py +++ b/tgtg_scanner/notifiers/base.py @@ -1,8 +1,8 @@ import logging from abc import ABC, abstractmethod -from models import Config, Cron, Item -from models.reservations import Reservation +from tgtg_scanner.models import Config, Cron, Item +from tgtg_scanner.models.reservations import Reservation log = logging.getLogger('tgtg') diff --git a/src/notifiers/console.py b/tgtg_scanner/notifiers/console.py similarity index 75% rename from src/notifiers/console.py rename to tgtg_scanner/notifiers/console.py index a2b69dc2..2b7d66e7 100644 --- a/src/notifiers/console.py +++ b/tgtg_scanner/notifiers/console.py @@ -1,8 +1,9 @@ import logging -from models import Config, Item -from models.errors import ConsoleConfigurationError, MaskConfigurationError -from notifiers.base import Notifier +from tgtg_scanner.models import Config, Item +from tgtg_scanner.models.errors import (ConsoleConfigurationError, + MaskConfigurationError) +from tgtg_scanner.notifiers.base import Notifier log = logging.getLogger('tgtg') diff --git a/src/notifiers/ifttt.py b/tgtg_scanner/notifiers/ifttt.py similarity index 83% rename from src/notifiers/ifttt.py rename to tgtg_scanner/notifiers/ifttt.py index b1cb531d..b87ee62d 100644 --- a/src/notifiers/ifttt.py +++ b/tgtg_scanner/notifiers/ifttt.py @@ -1,8 +1,9 @@ import logging -from models import Config, Item -from models.errors import IFTTTConfigurationError, MaskConfigurationError -from notifiers.webhook import WebHook +from tgtg_scanner.models import Config, Item +from tgtg_scanner.models.errors import (IFTTTConfigurationError, + MaskConfigurationError) +from tgtg_scanner.notifiers.webhook import WebHook log = logging.getLogger('tgtg') diff --git a/src/notifiers/notifiers.py b/tgtg_scanner/notifiers/notifiers.py similarity index 78% rename from src/notifiers/notifiers.py rename to tgtg_scanner/notifiers/notifiers.py index 28daa260..bf6cde6c 100644 --- a/src/notifiers/notifiers.py +++ b/tgtg_scanner/notifiers/notifiers.py @@ -1,18 +1,18 @@ import logging from typing import List -from models import Config, Cron, Favorites, Item, Reservations -from models.reservations import Reservation -from notifiers.apprise import Apprise -from notifiers.base import Notifier -from notifiers.console import Console -from notifiers.ifttt import IFTTT -from notifiers.ntfy import Ntfy -from notifiers.push_safer import PushSafer -from notifiers.script import Script -from notifiers.smtp import SMTP -from notifiers.telegram import Telegram -from notifiers.webhook import WebHook +from tgtg_scanner.models import Config, Cron, Favorites, Item, Reservations +from tgtg_scanner.models.reservations import Reservation +from tgtg_scanner.notifiers.apprise import Apprise +from tgtg_scanner.notifiers.base import Notifier +from tgtg_scanner.notifiers.console import Console +from tgtg_scanner.notifiers.ifttt import IFTTT +from tgtg_scanner.notifiers.ntfy import Ntfy +from tgtg_scanner.notifiers.push_safer import PushSafer +from tgtg_scanner.notifiers.script import Script +from tgtg_scanner.notifiers.smtp import SMTP +from tgtg_scanner.notifiers.telegram import Telegram +from tgtg_scanner.notifiers.webhook import WebHook log = logging.getLogger("tgtg") diff --git a/src/notifiers/ntfy.py b/tgtg_scanner/notifiers/ntfy.py similarity index 92% rename from src/notifiers/ntfy.py rename to tgtg_scanner/notifiers/ntfy.py index 904fa898..8a998dfe 100644 --- a/src/notifiers/ntfy.py +++ b/tgtg_scanner/notifiers/ntfy.py @@ -2,9 +2,10 @@ from requests.auth import HTTPBasicAuth -from models import Config, Item -from models.errors import MaskConfigurationError, NtfyConfigurationError -from notifiers.webhook import WebHook +from tgtg_scanner.models import Config, Item +from tgtg_scanner.models.errors import (MaskConfigurationError, + NtfyConfigurationError) +from tgtg_scanner.notifiers.webhook import WebHook log = logging.getLogger('tgtg') diff --git a/src/notifiers/push_safer.py b/tgtg_scanner/notifiers/push_safer.py similarity index 86% rename from src/notifiers/push_safer.py rename to tgtg_scanner/notifiers/push_safer.py index 6cf2720a..e97fa692 100644 --- a/src/notifiers/push_safer.py +++ b/tgtg_scanner/notifiers/push_safer.py @@ -2,9 +2,9 @@ from pushsafer import Client -from models import Config, Item -from models.errors import PushSaferConfigurationError -from notifiers.base import Notifier +from tgtg_scanner.models import Config, Item +from tgtg_scanner.models.errors import PushSaferConfigurationError +from tgtg_scanner.notifiers.base import Notifier log = logging.getLogger('tgtg') diff --git a/src/notifiers/script.py b/tgtg_scanner/notifiers/script.py similarity index 79% rename from src/notifiers/script.py rename to tgtg_scanner/notifiers/script.py index 6dae0818..2df2ae02 100644 --- a/src/notifiers/script.py +++ b/tgtg_scanner/notifiers/script.py @@ -1,9 +1,10 @@ import logging import subprocess -from models import Config, Item -from models.errors import MaskConfigurationError, ScriptConfigurationError -from notifiers import Notifier +from tgtg_scanner.models import Config, Item +from tgtg_scanner.models.errors import (MaskConfigurationError, + ScriptConfigurationError) +from tgtg_scanner.notifiers import Notifier log = logging.getLogger('tgtg') diff --git a/src/notifiers/smtp.py b/tgtg_scanner/notifiers/smtp.py similarity index 93% rename from src/notifiers/smtp.py rename to tgtg_scanner/notifiers/smtp.py index 1b262cee..7dc9f7c2 100644 --- a/src/notifiers/smtp.py +++ b/tgtg_scanner/notifiers/smtp.py @@ -4,9 +4,10 @@ from email.mime.text import MIMEText from smtplib import SMTPException, SMTPServerDisconnected -from models import Config, Item -from models.errors import MaskConfigurationError, SMTPConfigurationError -from notifiers.base import Notifier +from tgtg_scanner.models import Config, Item +from tgtg_scanner.models.errors import (MaskConfigurationError, + SMTPConfigurationError) +from tgtg_scanner.notifiers.base import Notifier log = logging.getLogger('tgtg') diff --git a/src/notifiers/telegram.py b/tgtg_scanner/notifiers/telegram.py similarity index 97% rename from src/notifiers/telegram.py rename to tgtg_scanner/notifiers/telegram.py index 0254fe2a..181c9d0a 100644 --- a/src/notifiers/telegram.py +++ b/tgtg_scanner/notifiers/telegram.py @@ -11,11 +11,13 @@ CommandHandler, Filters, MessageHandler, Updater) from telegram.utils.helpers import escape_markdown -from models import Config, Favorites, Item, Reservations -from models.errors import MaskConfigurationError, TelegramConfigurationError -from models.favorites import AddFavoriteRequest, RemoveFavoriteRequest -from models.reservations import Order, Reservation -from notifiers.base import Notifier +from tgtg_scanner.models import Config, Favorites, Item, Reservations +from tgtg_scanner.models.errors import (MaskConfigurationError, + TelegramConfigurationError) +from tgtg_scanner.models.favorites import (AddFavoriteRequest, + RemoveFavoriteRequest) +from tgtg_scanner.models.reservations import Order, Reservation +from tgtg_scanner.notifiers.base import Notifier log = logging.getLogger('tgtg') @@ -187,7 +189,7 @@ def _reserve_item_menu(self, update: Update, context: CallbackContext) -> None: del context - favorites = self.reservations.get_favorites() + favorites = self.favorites.get_favorites() buttons = [[ InlineKeyboardButton( f"{item.display_name}: {item.items_available}", diff --git a/src/notifiers/webhook.py b/tgtg_scanner/notifiers/webhook.py similarity index 92% rename from src/notifiers/webhook.py rename to tgtg_scanner/notifiers/webhook.py index 974ab339..90f94ff2 100644 --- a/src/notifiers/webhook.py +++ b/tgtg_scanner/notifiers/webhook.py @@ -4,9 +4,10 @@ import requests from requests.auth import HTTPBasicAuth -from models import Config, Item -from models.errors import MaskConfigurationError, WebHookConfigurationError -from notifiers.base import Notifier +from tgtg_scanner.models import Config, Item +from tgtg_scanner.models.errors import (MaskConfigurationError, + WebHookConfigurationError) +from tgtg_scanner.notifiers.base import Notifier log = logging.getLogger('tgtg') diff --git a/src/scanner.py b/tgtg_scanner/scanner.py similarity index 96% rename from src/scanner.py rename to tgtg_scanner/scanner.py index fa069021..7f345544 100644 --- a/src/scanner.py +++ b/tgtg_scanner/scanner.py @@ -6,11 +6,11 @@ from progress.spinner import Spinner -from models import (Config, Cron, Favorites, Item, Location, Metrics, - Reservations) -from models.errors import TgtgAPIError -from notifiers import Notifiers -from tgtg import TgtgClient +from tgtg_scanner.models import (Config, Cron, Favorites, Item, Location, + Metrics, Reservations) +from tgtg_scanner.models.errors import TgtgAPIError +from tgtg_scanner.notifiers import Notifiers +from tgtg_scanner.tgtg import TgtgClient log = logging.getLogger("tgtg") @@ -290,4 +290,4 @@ def unset_all_favorites(self) -> None: if __name__ == "__main__": - print("Please use main.py.") + print("Please use __main__.py.") diff --git a/src/tgtg/LICENCE b/tgtg_scanner/tgtg/LICENCE similarity index 100% rename from src/tgtg/LICENCE rename to tgtg_scanner/tgtg/LICENCE diff --git a/tgtg_scanner/tgtg/__init__.py b/tgtg_scanner/tgtg/__init__.py new file mode 100644 index 00000000..af9c1bbb --- /dev/null +++ b/tgtg_scanner/tgtg/__init__.py @@ -0,0 +1,3 @@ +from tgtg_scanner.tgtg.tgtg_client import TgtgClient + +__all__ = ['TgtgClient'] diff --git a/src/tgtg/tgtg_client.py b/tgtg_scanner/tgtg/tgtg_client.py similarity index 98% rename from src/tgtg/tgtg_client.py rename to tgtg_scanner/tgtg/tgtg_client.py index 59149169..6fe1b568 100644 --- a/src/tgtg/tgtg_client.py +++ b/tgtg_scanner/tgtg/tgtg_client.py @@ -14,8 +14,8 @@ from requests.adapters import HTTPAdapter from urllib3.util import Retry -from models.errors import (TgtgAPIError, TGTGConfigurationError, - TgtgLoginError, TgtgPollingError) +from tgtg_scanner.models.errors import (TgtgAPIError, TGTGConfigurationError, + TgtgLoginError, TgtgPollingError) log = logging.getLogger("tgtg") BASE_URL = "https://apptoogoodtogo.com/api/" @@ -390,7 +390,7 @@ def create_order(self, item_id: str, item_count: int) -> dict: json={"item_count": item_count}) if response.json().get("state") != "SUCCESS": raise TgtgAPIError(response.status_code, response.content) - return response.json().get("order") + return response.json().get("order", {}) def get_order_status(self, order_id: str) -> dict: self.login() diff --git a/wiki/Configuration.md b/wiki/Configuration.md new file mode 100644 index 00000000..39736b82 --- /dev/null +++ b/wiki/Configuration.md @@ -0,0 +1,210 @@ + +## Basic configuration + +The only required option is the Email aka Username to your TGTG account. + +A minimalistic configuration could look like this: + +```ini +[TGTG] +Username = my_mail@example.com +``` + +This config will start the scanner and lead you through the login process. +After the successful login, you will see changes in the available amounts of your favorite magic bags in the console window. + +## Variables + +Some of the following options allow the inclusion of special variables that contain item (magic bag) information. + +Example to include the display name of the item: `${{display_name}}` + +Variables with the `locale` property are affected by the `locale` option and returned in the given language. + +| variable | description | example | locale | +|----------|-------------|---------|--------| +| item_id | unique identifier of the item | `774625` | +| items_available | number of available items | `2` | +| display_name | name of the item as in the APP | `Chutney Indian Food - Hamburg – Europapassage 2.OG` | +| description | item description | `Rette eine Magic Bag mit leckerem indischen Essen.` | +| price | item price including taxes | `3.20` | +| currency | | `EUR` | +| pickupdate | formatted string | `tomorrow, 18:00 - 21:50` | YES | +| favorite | is favorite | `YES` or `NO` | +| rating | overall rating | `3.3` | +| buffet | is buffet | `YES` or `NO` | +| item_category | | `MEAL` | +| item_name | | +| packaging_option | | `BAG_ALLOWED` | +| pickup_location | | `Ballindamm 40, 20095 Hamburg, Deutschland` | +| store_name | | `Chutney Indian Food` | +| item_logo | item logo url | `https://tgtg-mkt-cms-prod.s3.eu-west-1.amazonaws.com/13512/TGTG_Icon_White_Cirle_1988x1988px_RGB.png` | +| item_cover | item cover url | `https://images.tgtg.ninja/standard_images/GENERAL/other1.jpg` | +| scanned_on | timestamp when the item was scanned | `2023-02-14 20:43:21` | +| item_logo_bytes | item logo as data blob | +| item_cover_bytes | item cover as data blob | +| link | url of the item | `https://share.toogoodtogo.com/item/774625` | +| distance_walking | walking distance from home | `5.9 km` | YES | +| distance_driving | driving distance from home | `8 km` | YES | +| distance_transit | transit distance from home | `8 km` | YES | +| distance_biking | biking distance from home | `6.1 km` | YES | +| duration_walking | walking duration from home | `1 hour` | YES | +| duration_driving | driving duration from home | `20 minutes` | YES | +| duration_transit | transit duration from home | `45 minutes` | YES | +| duration_biking | biking duration from home | `30 minutes` | YES | + +## Cron Scheduler + +For formatting support see: + +You can combine multiple crons as semicolon separated list. + +## Available options + +### [MAIN] / general settings + +| config.ini | environment | description | default | +|------------|-------------|-------------|---------| +| Debug | DEBUG | enable debugging mode | `false` | +| SleepTime | SLEEP_TIME | time between two consecutive scans in seconds | `60` | +| ScheduleCron | SCHEDULE_CRON | run only on schedule | `* * * * *` | +| ItemIDs | ITEM_IDS | **Depreciated!** comma-separated list of additional (none favorite) items to scan | +| Metrics | METRICS | enable Prometheus metrics HTTP server | `false` | +| MetricsPort | METRICS_PORTS | port for metrics server | `8000` | +| DisableTests | DISABLE_TESTS | disable test notifications on startup | `false` | +| Quiet | QUIET | minimal console output | `false` | +| Locale | LOCALE | localization | `en_US` | +| Activity | ACTIVITY | show running indicator (always disabled in docker) | `true` | +| | TZ | timezone for docker based setups, e.g. `Berlin/Europe` | +| | UID | set user id for docker container | `1000` | +| | GID | set group id for docker container | `1000` | + +### [TGTG] / TGTG account + +| config.ini | environment | description | default | required | +|------------|-------------|-------------|---------|:----------:| +| Username | TGTG_USERNAME | email connected to your TGTG Account | | YES | +| AccessToken | TGTG_ACCESS_TOKEN | TGTG API access token | +| RefreshToken | TGTG_REFRESH_TOKEN | TGTG API refresh token | +| UserId | TGTG_USER_ID | TGTG API user ID | +| Datadome | TGTG_DATADOME | TGTG API datadome protection cookie | +| Timeout | TGTG_TIMEOUT | timeout for API requests | `60` | +| AccessTokenLifetime | TGTG_ACCESS_TOKEN_LIFETIME | access token lifetime in seconds | `14400` | +| MaxPollingTries | TGTG_MAX_POLLING_TRIES | max polling retries during login | `24` | +| PollingWaitTime | TGTG_POLLING_WAIT_TIME | time between polling retries in seconds | `5` | + +### [LOCATION] / Location settings + +| config.ini | environment | description | default | required if enabled | variables | +|------------|-------------|-------------|---------|:-------------------:|:---------:| +| Enabled | LOCATION | enable location service | `false` | +| Google_Maps_API_Key | LOCATION_GOOGLE_MAPS_API_KEY | API key for google maps service | | YES | +| Address | LOCATION_ADDRESS | origin for distance calculation, e.g. your home address | | YES | + +### [CONSOLE] / Console Notifier + +| config.ini | environment | description | default | required if enabled | variables | +|------------|-------------|-------------|---------|:-------------------:|:---------:| +| Enabled | CONSOLE | enable console notifications | `false` | | +| Body | CONSOLE_BODY | message body | `${{scanned_on}} ${{display_name}} - new amount: ${{items_available}}` | | YES | +| Cron | CONSOLE_CRON | enable notification only on schedule | `* * * * *` | + +### [SMTP] / SMTP Notifier + +| config.ini | environment | description | default | required if enabled | variables | +|------------|-------------|-------------|---------|:-------------------:|:---------:| +| Enabled | SMTP | enable SMTP notifications | `false` | +| Host | SMTP_HOST | SMTP server host | `smtp.gmail.com` | +| Port | SMTP_PORT | SMTP server port | `587` | +| TLS | SMTP_TLS | enable TLS | `true`| +| SSL | SMTP_SSL | enable SSL | `false` | +| Username | SMTP_USERNAME | login username | +| Password | SMTP_PASSWORD | login password | +| Sender | SMTP_SENDER | email sender | +| Recipient | SMTP_RECIPIENT | email recipient | | YES | +| Subject | SMTP_SUBJECT | email subject | `New Magic Bags` | | YES | +| Body | SMTP_BODY | email html body | `${{display_name}}
New Amount: ${{items_available}}` | | YES | +| Cron | SMTP_CRON | enable notification only on schedule | `* * * * *` | + +### [PUSHSAFER] / Pushsafer Notifier + +| config.ini | environment | description | default | required if enabled | variables | +|------------|-------------|-------------|---------|:-------------------:|:---------:| +| Enabled | PUSH_SAFER | enable Pushsafer notifications | `false` | +| Key | PUSH_SAFER_KEY | Pushsafer API key | | YES | +| DeviceID | PUSH_SAFER_DEVICE_ID | Pushsafer device ID | | YES | +| Cron | PUSH_SAFER_CRON | enable notification only on schedule | `* * * * *` | + +### [IFTTT] / IFTTT Notifier + +| config.ini | environment | description | default | required if enabled | variables | +|------------|-------------|-------------|---------|:-------------------:|:---------:| +| Enabled | IFTTT | enable IFTTT notifications | `false` | +| Event | IFTTT_EVENT | IFTTT webhook event | | YES | +| Key | IFTTT_KEY | IFTTT webhook key | | YES | +| Body | IFTTT_BODY | JSON message body | `{"value1": "${{display_name}}", "value2": ${{items_available}}, "value3": "${{link}}"}` | | YES | +| Timeout | IFTTT_TIMEOUT | timeout for API requests | 60 | +| Cron | IFTTT_CRON | enable notification only on schedule | `* * * * *` | + +### [TELEGRAM] / Telegram Notifier + +| config.ini | environment | description | default | required if enabled | variables | +|------------|-------------|-------------|---------|:-------------------:|:---------:| +| Enabled | TELEGRAM | enable Telegram notifications | `false` | +| Token | TELEGRAM_TOKEN | Telegram Bot token | | YES | +| Chat_IDs | TELEGRAM_CHAT_IDS | comma-separated list of chat ids | +| Body | TELEGRAM_BODY | message body | `*${{display_name}}* \n*Available*: ${{items_available}}\n*Price*: ${{price}} ${{currency}}\n*Pickup*: ${{pickupdate}}` | | YES +| disableCommands | TELEGRAM_DISABLE_COMMANDS | disable bot commands | `false` | +| Timeout | TELEGRAM_TIMEOUT | timeout for telegram API requests | 60 | +| Cron | TELEGRAM_CRON | enable notification only on schedule | `* * * * *` | + +#### Note on Markdown V2 + +As of Version 1.17.0 the Telegram Notifier uses the Markdown V2 parser of the Telegram API. +This requires all special markdown characters, that should not be parsed as markdown commands, +to be escaped with a preceding `\`. +The special characters are `_`, `*`, `[`, `]`, `(`, `)`, `~`, `` ` ``, `>`, `#`, `+`, `-`, `=`, `|`, `{`, `}`, `.` and `!`. + +### [APPRISE] / Apprise Notifier + +For details on the service URL configuration see . + +| config.ini | environment | description | default | required if enabled | variables | +|------------|-------------|-------------|---------|:-------------------:|:---------:| +| Enabled | APPRISE | enable Apprise notifications | `false` | +| URL | APPRISE_URL | Service URL | | YES | +| Title | APPRISE_TITLE | Notification title | `New Magic Bags` | | YES | +| Body | APPRISE_BODY | Notification body | `${{display_name}} - new amount: ${{items_available}} - ${{link}}` | | YES | +| Cron | APPRISE_CRON | enable notification only on schedule | `* * * * *` | + +### [NTFY] / Ntfy Notifier + +| config.ini | environment | description | default | required if enabled | variables | +|------------|-------------|-------------|---------|:-------------------:|:---------:| +| Enabled | NTFY | enable Ntfy notifications | `false` | +| Server | NTFY_SERVER | Ntfy server URL | `https://ntfy.sh` | YES | +| Topic | NTFY_TOPIC | Ntfy topic | | YES | +| Title | NTFY_TITLE | Notification title | `New TGTG items` | | YES | +| Message | NTFY_MESSAGE | Notification message | `${{display_name}} - New Amount: ${{items_available}} - ${{itelinkm_id}}` | | YES | +| Priority | NTFY_PRIORITY | | `default` | +| Tags | NTFY_TAGS | comma-separated list of tags | `shopping,tgtg` | | YES | +| Click | NTFY_CLICK | URL to open on click | `${{link}}` | | YES | +| Username | NTFY_USERNAME | auth username | +| Password | NTFY_PASSWORD | auth password | +| Timeout | NTFY_TIMEOUT | timeout for Ntfy requests | 60 | +| Cron | NTFY_CRON | enable notification only on schedule | `* * * * *` | + +### [WEBHOOK] / Webhook Notifier + +| config.ini | environment | description | default | required if enabled | variables | +|------------|-------------|-------------|---------|:-------------------:|:---------:| +| Enabled | WEBHOOK | enable Webhook notifications | `false` | +| URL | WEBHOOK_URL | webhook endpoint | | YES | +| Method | WEBHOOK_METHOD | request method | `POST` | +| Body | WEBHOOK_BODY | request body | `''` | | YES | +| Type | WEBHOOK_TYPE | request content type | `text/plain` | +| Headers | WEBHOOK_HEADERS | additional request headers as JSON | `{}` | +| Username | WEBHOOK_USERNAME | basic authentication username | +| Password | WEBHOOK_PASSWORD | basic authentication password | +| Timeout | WEBHOOK_TIMEOUT | request timeout | `60` | +| Cron | WEBHOOK_CRON | enable notification only on schedule | `* * * * *` | diff --git a/wiki/Examples.md b/wiki/Examples.md new file mode 100644 index 00000000..ba4e28bf --- /dev/null +++ b/wiki/Examples.md @@ -0,0 +1,69 @@ + +## Webhook Openhab example + +This configuration triggers an openhab switch every time new items are available. +I use it to make a LED lightstripe flash. + +`docker-compose.yml` config: + +```yaml +environment: +- WEBHOOK=true +- WEBHOOK_URL=http://openhab.domain/rest/items/TGTG_New_Item +- WEBHOOK_METHOD=POST +- WEBHOOK_BODY=ON +- WEBHOOK_TIMEOUT=60 +``` + +Openhab item file `tgtg.items`: + +```c +Switch TGTG_New_Item { expire="10s,command=OFF" } +``` + +You can expand the expire time to set the minimum interval between consecutive +events that will be triggered. + +Openhab item file `led.items`: + +```c +Group LED_stripe + +Switch LED_stripe_power "Power [%s]" (LED_stripe) {channel="wifiled:wifiled:wohnzimmer:power"} +Dimmer LED_stripe_white "White [%s]" (LED_stripe) {channel="wifiled:wifiled:wohnzimmer:white2"} +Color LED_stripe_color "Color [%s]" (LED_stripe) {channel="wifiled:wifiled:wohnzimmer:color"} + +// Temp Items +Switch LED_stripe_power_temp "temp Power [%s]" (LED_stripe) +Color LED_stripe_color_temp "temp Color [%s]" (LED_stripe) +Dimmer LED_stripe_white_temp "temp White [%s]" (LED_stripe) +``` + +Of cause you have to adapt your channel. + +Openhab rule file `tgtg.rules`: + +```c +rule "TGTG Notification" +when + Item TGTG_New_Item changed from OFF +then + LED_stripe_color_temp.sendCommand(LED_stripe_color.state.toString) + LED_stripe_power_temp.sendCommand(LED_stripe_power.state.toString) + var i = 0 + while ((i=i+1) <= 3){ + LED_stripe_power.sendCommand(OFF) + LED_stripe_color.sendCommand("0,100,100") + Thread::sleep(800) + LED_stripe_power.sendCommand(OFF) + LED_stripe_color.sendCommand(LED_stripe_color_temp.state.toString) + LED_stripe_power.sendCommand(LED_stripe_power_temp.state.toString) + Thread::sleep(800) + } + Thread::sleep(800) + LED_stripe_power.sendCommand(LED_stripe_power_temp.state.toString) +end +``` + +This will save the current state of the LED stripe, make 3 red flashes +and reset the previous settings. diff --git a/wiki/FAQ.md b/wiki/FAQ.md new file mode 100644 index 00000000..dcb4b3d7 --- /dev/null +++ b/wiki/FAQ.md @@ -0,0 +1,62 @@ + +## 1. I am getting Error 403 all the time + +### Cause + +If you see this error you where blocked from using the TGTG API. +It is usually caused by a too high request rate. +Since tools like this scanner violate the terms and conditions of TGTG +they try to detect the corresponding request patterns and block them. +These bans are usually temporary. + +### Possible Solutions + +1. Make sure you are using the latest version of the tgtg-scanner. +2. Increase the sleepTime (`config.ini`) or `SLEEP_TIME` (environment variable) between API requests. +3. Make sure you don't run multiple instances of the tgtg-scanner at the same time. +4. Try using a different IP. Most ISP provide a new IP when you reconnect your internet connection. +5. Wait some time. In most cases the error disappears after 24h. + +## 2. How do I set up the tgtg-scanner in the Synology DSM Docker app? + +Create the container as described in the official documentation +[Creating a Container](https://kb.synology.com/en-us/DSM/help/Docker/docker_container?version=7). +Set up the environment as described in the `docker-compose.yml`. +You don't have to set up any volumes. +The container will create a volume for persistent storage of your tgtg credentials by itself. + +If you see the error `ERROR SAVING CREDENTIALS! - [ERRNO 13]` in your log +([#310](https://github.com/Der-Henning/tgtg/issues/310)), +the scanner has no permission to write the token files into the `/token` directory. +To fix the permission run `chown 1001:1001 /volume1/docker/tgtg/tokens` on your Synology. +You may need to adjust the path according to your setup. + +## 3. How do I create a Telegram Bot? + +You can create a new bot using @BotFather. +Simply start a conversation with @BotFather and send `/newbot`. +You will receive an API token for the bot. Enter this token in your config. +On the next start, the scanner will help you obtain the chat_id. + +For more information about the @BotFather please refer to the official documentation: + + +## 4. How does the reservation feature work? + +Currently, the reservation feature only works with the telegram bot included in the telegram notifier. +At the moment the bot cannot buy a magic bag. +It can only reserve a magic bag and hold it for up to 5 minutes. +After this time or when you cancel the reservation the bag will be available again. + +To buy the magic bag you have to cancel the reservation with the telegram bot, +which makes the item available again. +Now you can click and buy it in the official TGTG app. + +The bot implements the following commands. + ++ \reserve: Lists all your favorite magic bags. +Clicking on each of the items will trigger a reservation for one bag as soon as it is available. ++ \reservations: Lists all magic bags you activated with \reserve that have not yet been triggered. +Click to cancel the reservation for the next available bag. ++ \orders: Lists all triggered and active reservations. Click to cancel the reservation. ++ \cancelall: Cancel all active reservations. diff --git a/wiki/Home.md b/wiki/Home.md new file mode 100644 index 00000000..ca0014f9 --- /dev/null +++ b/wiki/Home.md @@ -0,0 +1,17 @@ + +## Welcome to the TGTG Scanner wiki + +1. [Configuration](Configuration.md) + 1. [Basic configuration](Configuration.md#basic-configuration) + 2. [Variables](Configuration.md#variables) + 3. [Cron scheduler](Configuration.md#cron-scheduler) + 4. [Available options](Configuration.md#available-options) + +2. [FAQ](FAQ.md) + 1. [I am getting Error 403 all the time.](FAQ.md#1-i-am-getting-error-403-all-the-time) + 2. [How do I setup the tgtg-scanner in the Synology DSM Docker app?](FAQ.md#2-how-do-i-setup-the-tgtg-scanner-in-the-synology-dsm-docker-app) + 3. [How do I create a Telegram Bot?](FAQ.md#3-how-do-i-create-a-telegram-bot) + 4. [How does the reservation feature work?](FAQ.md#4-how-does-the-reservation-feature-work) + +3. [Examples](Examples.md) + 1. [Webhook Openhab example](Examples.md#webhook-openhab-example)