main 87c4e739345b cached
33 files
80.9 KB
19.1k tokens
50 symbols
1 requests
Download .txt
Repository: ranjan-mohanty/vfs-appointment-bot
Branch: main
Commit: 87c4e739345b
Files: 33
Total size: 80.9 KB

Directory structure:
gitextract_xbbv28z9/

├── .flake8
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── dependabot.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── build.yml
│       ├── codeql.yml
│       ├── dependency-review.yml
│       ├── publish-testpypi.yml
│       ├── publish.yml
│       └── scorecard.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── pyproject.toml
└── vfs_appointment_bot/
    ├── main.py
    ├── notification/
    │   ├── email_client.py
    │   ├── notification_client.py
    │   ├── notification_client_factory.py
    │   ├── telegram_client.py
    │   └── twilio_client.py
    ├── utils/
    │   ├── config_reader.py
    │   ├── date_utils.py
    │   └── timer.py
    └── vfs_bot/
        ├── vfs_bot.py
        ├── vfs_bot_de.py
        ├── vfs_bot_factory.py
        └── vfs_bot_it.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .flake8
================================================
[flake8]
exclude = 
    .git,
    .github,
    __pycache__,
    venv,
    dist,
    config,
    build

max-complexity = 10
max-line-length = 120

================================================
FILE: .github/FUNDING.yml
================================================
github: ranjan-mohanty

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: ""
assignees: ""
---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:

1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**

- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]

**Smartphone (please complete the following information):**

- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ""
labels: ""
assignees: ""
---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "pip"
    directory: "/"
    schedule:
      interval: "weekly"
    ignore:
      - dependency-name: "*"
        update-types: ["version-update:semver-patch"]

  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: daily
    ignore:
      - dependency-name: "*"
        update-types: ["version-update:semver-patch"]


================================================
FILE: .github/pull_request_template.md
================================================
## Pull Request Template

**Thank you for contributing to the VFS appointment bot project!**

To streamline the review process, please fill out the details below when creating a pull request.

**Title:**

- Briefly describe your changes here

**Description:**

- What changes did you make?
  - Explain the purpose of your changes and how they address an issue or improve the scraper.
  - If applicable, mention any new features you've implemented.
  - Include steps to reproduce any bug fixes (if applicable).
- How did you test your changes?
  - Did you add new unit tests?
  - Did you manually test with different scenarios?

**Additional Information:**

- Did you add any dependencies or external libraries?
- Did you modify the documentation? If so, how?

**Checklist:**

- [ ] I followed the Coding Style Guide: [https://peps.python.org/pep-0008/](https://peps.python.org/pep-0008/).
- [ ] I added clear and concise comments to my code.
- [ ] I added unit tests for new features or bug fixes (if applicable).
- [ ] I ensured my changes do not introduce regressions.

**Optional:**

- Issue reference (if related to an existing issue): # (issue number)

**By providing these details, you'll help us review your pull request efficiently. We appreciate your contributions!**


================================================
FILE: .github/workflows/build.yml
================================================
name: Build
on:
  workflow_call:
  push:
    branches:
      - "**"
    tags-ignore:
      - "**"

permissions:
  contents: read

jobs:
  build:
    name: Build distribution
    runs-on: ubuntu-latest

    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
        with:
          egress-policy: audit

      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
      - name: Set up Python
        uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
        with:
          python-version: "3.x"
      - name: Install pypa/build
        run: python3 -m pip install build --user
      - name: Build a binary wheel and a source tarball
        run: python3 -m build
      - name: Store the distribution packages
        uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
        with:
          name: python-package-distributions
          path: dist/


================================================
FILE: .github/workflows/codeql.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"

on:
  push:
    branches: ["main"]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: ["main"]
  schedule:
    - cron: "0 0 * * 1"

permissions:
  contents: read

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: ["python"]
        # CodeQL supports [ $supported-codeql-languages ]
        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support

    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
        with:
          egress-policy: audit

      - name: Checkout repository
        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7

      # Initializes the CodeQL tools for scanning.
      - name: Initialize CodeQL
        uses: github/codeql-action/init@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5
        with:
          languages: ${{ matrix.language }}
          # If you wish to specify custom queries, you can do so here or in a config file.
          # By default, queries listed here will override any specified in a config file.
          # Prefix the list here with "+" to use these queries and those in the config file.

      # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
      # If this step fails, then you should remove it and run the build manually (see below)
      - name: Autobuild
        uses: github/codeql-action/autobuild@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5

      # ℹ️ Command-line programs to run using the OS shell.
      # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun

      #   If the Autobuild fails above, remove it and uncomment the following three lines.
      #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.

      # - run: |
      #   echo "Run, Build Application using script"
      #   ./location_of_script_within_repo/buildscript.sh

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5
        with:
          category: "/language:${{matrix.language}}"


================================================
FILE: .github/workflows/dependency-review.yml
================================================
# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request,
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
# Once installed, if the workflow run is marked as required,
# PRs introducing known-vulnerable packages will be blocked from merging.
#
# Source repository: https://github.com/actions/dependency-review-action
name: 'Dependency Review'
on: [pull_request]

permissions:
  contents: read

jobs:
  dependency-review:
    runs-on: ubuntu-latest
    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
        with:
          egress-policy: audit

      - name: 'Checkout Repository'
        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
      - name: 'Dependency Review'
        uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4


================================================
FILE: .github/workflows/publish-testpypi.yml
================================================
name: Publish - TestPyPI
on:
  push:
    tags:
      - '**'

permissions:
  contents: read

jobs:
  build:
    uses: ./.github/workflows/build.yml
  publish-to-testpypi:
    name: Publish to TestPyPI
    needs:
      - build
    runs-on: ubuntu-latest

    environment:
      name: testpypi
      url: https://test.pypi.org/p/vfs-appointment-bot

    permissions:
      id-token: write

    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
        with:
          egress-policy: audit

      - name: Download all the dists
        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
        with:
          name: python-package-distributions
          path: dist/
      - name: Publish distribution to TestPyPI
        uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # release/v1
        with:
          repository-url: https://test.pypi.org/legacy/
  github-release:
    if: ${{contains(github.ref, 'rc')}}
    name: Release
    needs:
      - publish-to-testpypi
    runs-on: ubuntu-latest

    permissions:
      contents: write
      id-token: write

    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
        with:
          egress-policy: audit

      - name: Download all the dists
        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
        with:
          name: python-package-distributions
          path: dist/
      - name: Sign the dists with Sigstore
        uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0
        with:
          inputs: >-
            ./dist/*.tar.gz
            ./dist/*.whl
      - name: Create GitHub Release
        env:
          GITHUB_TOKEN: ${{ github.token }}
        run: >-
          gh release create
          '${{ github.ref_name }}'
          --repo '${{ github.repository }}'
          --prerelease
      - name: Upload artifact signatures to GitHub Release
        env:
          GITHUB_TOKEN: ${{ github.token }}
        run: >-
          gh release upload
          '${{ github.ref_name }}' dist/**
          --repo '${{ github.repository }}'


================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish - PyPI
on:
  push:
    tags-ignore:
      - '*rc*'

permissions:
  contents: read

jobs:
  build:
    uses: ./.github/workflows/build.yml
  publish-to-pypi:
    name: Publish to PyPI
    needs:
      - build
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/vfs-appointment-bot
    permissions:
      id-token: write

    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
        with:
          egress-policy: audit

      - name: Download all the dists
        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
        with:
          name: python-package-distributions
          path: dist/
      - name: Publish distribution to PyPI
        uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # release/v1

  github-release:
    name: Release
    needs:
      - publish-to-pypi
    runs-on: ubuntu-latest

    permissions:
      contents: write
      id-token: write

    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
        with:
          egress-policy: audit

      - name: Download all the dists
        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
        with:
          name: python-package-distributions
          path: dist/
      - name: Sign the dists with Sigstore
        uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0
        with:
          inputs: >-
            ./dist/*.tar.gz
            ./dist/*.whl
      - name: Create GitHub Release
        env:
          GITHUB_TOKEN: ${{ github.token }}
        run: >-
          gh release create
          '${{ github.ref_name }}'
          --repo '${{ github.repository }}'
      - name: Upload artifact signatures to GitHub Release
        env:
          GITHUB_TOKEN: ${{ github.token }}
        run: >-
          gh release upload
          '${{ github.ref_name }}' dist/**
          --repo '${{ github.repository }}'


================================================
FILE: .github/workflows/scorecard.yml
================================================
# This workflow uses actions that are not certified by GitHub. They are provided
# by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation.

name: Scorecard Security
on:
  push:
    branches: ["main"]

# Declare default permissions as read only.
permissions: read-all

jobs:
  analysis:
    name: Scorecard analysis
    runs-on: ubuntu-latest
    permissions:
      # Needed to upload the results to code-scanning dashboard.
      security-events: write
      # Needed to publish results and get a badge (see publish_results below).
      id-token: write
      # Uncomment the permissions below if installing in a private repository.
      # contents: read
      # actions: read

    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
        with:
          egress-policy: audit

      - name: "Checkout code"
        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
        with:
          persist-credentials: false

      - name: "Run analysis"
        uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
        with:
          results_file: results.sarif
          results_format: sarif
          # (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
          # - you want to enable the Branch-Protection check on a *public* repository, or
          # - you are installing Scorecard on a *private* repository
          # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
          # repo_token: ${{ secrets.SCORECARD_TOKEN }}

          # Public repositories:
          #   - Publish results to OpenSSF REST API for easy access by consumers
          #   - Allows the repository to include the Scorecard badge.
          #   - See https://github.com/ossf/scorecard-action#publishing-results.
          # For private repositories:
          #   - `publish_results` will always be set to `false`, regardless
          #     of the value entered here.
          publish_results: true

      # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
      # format to the repository Actions tab.
      - name: "Upload artifact"
        uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
        with:
          name: SARIF file
          path: results.sarif
          retention-days: 5

      # Upload the results to GitHub's code scanning dashboard.
      - name: "Upload to code-scanning"
        uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5
        with:
          sarif_file: results.sarif


================================================
FILE: .gitignore
================================================
# Distribution / packaging
dist/
eggs/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
poetry.lock

# Unit test / coverage reports
.coverage
.coverage.*
.pytest_cache/

# Translations
*.mo
*.pot

# IDEs/Build Tools (consider including if you use them)
.idea/
.vscode/
build/

# Virtual Environments
.venv/
env/
venv

# Logs
app.log

# Local Config
config.ini
*.ini

================================================
FILE: .pre-commit-config.yaml
================================================
repos:
- repo: https://github.com/gitleaks/gitleaks
  rev: v8.16.3
  hooks:
  - id: gitleaks
- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v4.4.0
  hooks:
  - id: end-of-file-fixer
  - id: trailing-whitespace
- repo: https://github.com/pylint-dev/pylint
  rev: v2.17.2
  hooks:
  - id: pylint
- repo: https://github.com/pycqa/flake8
  rev: ^4.0.1
  hooks:
  - id: flake8
- repo: https://github.com/psf/black
  rev: ^22.3.2
  hooks:
  - id: black


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
  overall community

Examples of unacceptable behavior include:

- The use of sexualized language or imagery, and sexual attention or
  advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
  address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<ranjan@duck.com>.
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series
of actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within
the community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
version 2.0, available [here](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html).

Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).

For answers to common questions about this code of conduct, see the FAQ [here](https://www.contributor-covenant.org/faq). Translations are available [here](https://www.contributor-covenant.org/translations).


================================================
FILE: CONTRIBUTING.md
================================================
## Welcome to Amazon Product Details Scraper contributing guide

Thank you for investing your time in contributing to our project! We welcome contributions to this project! Here's how you can get involved:

**1. Issues:**

- **Bug reports:** If you encounter a bug, please search existing issues first to avoid duplicates. If you can't find a relevant issue, create a new one with a clear description of the problem, including steps to reproduce it.
- **Feature requests:** If you have an idea for a new feature, feel free to create an issue to discuss it. Explain the functionality you'd like to see and how it would benefit the project.

**2. Pull Requests:**

- **Fork the repository:** Create a fork of the repository on GitHub.
- **Clone your fork:** Clone your forked repository to your local machine.
- **Create a new branch:** Create a new branch for your changes (e.g., `feature/your_feature_name`).
- **Implement your changes:** Make your modifications to the code.
- **Write clear commit messages:** Use descriptive commit messages that explain your changes.
- **Test your changes:** Ensure your changes don't introduce regressions. Consider adding unit tests if applicable.
- **Push to your branch:** Push your changes to your branch on your forked repository.
- **Create a pull request:** Create a pull request from your branch to the main branch of the upstream repository.
- **Address feedback:** We will review your pull request and provide feedback. Be prepared to address any comments or suggestions.

**3. Coding Style:**

- Follow [PEP 8 style guide](https://peps.python.org/pep-0008/) for Python code.
- Use consistent formatting and indentation.

**4. License:**

- This project is licensed under the MIT License. Please ensure your contributions are compatible with the license.

**5. Code Documentation:**

- Add comments to document your code, especially complex sections.
- Consider using docstrings to explain functions and classes.

**Thank you for your contributions!**

**Additional Notes:**

- You are not obligated to include all the sections mentioned above. Adapt them to your project's specific needs.
- Feel free to add a section about setting up development environment if your project has dependencies.
- Consider including a code of conduct to promote a positive and inclusive community.


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022 Ranjan Mohanty

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# VFS Appointment Bot

[![GitHub License](https://img.shields.io/github/license/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/blob/main/LICENSE)
[![GitHub Release](https://img.shields.io/github/v/release/ranjan-mohanty/vfs-appointment-bot?logo=GitHub)](https://github.com/ranjan-mohanty/vfs-appointment-bot/releases)
[![PyPI - Version](https://img.shields.io/pypi/v/vfs-appointment-bot?logo=pypi)](https://pypi.org/project/vfs-appointment-bot)
[![Downloads](https://static.pepy.tech/badge/vfs-appointment-bot)](https://pepy.tech/project/vfs-appointment-bot)
[![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fhits.dwyl.com%2Franjan-mohanty%2Fvfs-appointment-bot.json&style=flat&logo=GitHub&label=views)](https://github.com/ranjan-mohanty/vfs-appointment-bot)
[![GitHub forks](https://img.shields.io/github/forks/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/forks)
[![GitHub Repo stars](https://img.shields.io/github/stars/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/stargazers)

[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ranjan-mohanty/vfs-appointment-bot/build.yml)](https://github.com/ranjan-mohanty/vfs-appointment-bot/actions/workflows/build.yml)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/21f1ecd428ec4342980020a6ef383439)](https://app.codacy.com/gh/ranjan-mohanty/vfs-appointment-bot/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ranjan-mohanty/vfs-appointment-bot/badge)](https://securityscorecards.dev/viewer/?uri=github.com/ranjan-mohanty/vfs-appointment-bot)
[![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/issues)
![Libraries.io dependency status for GitHub repo](https://img.shields.io/librariesio/github/ranjan-mohanty/vfs-appointment-bot)
[![Twitter](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Franjan-mohanty%2Fvfs-appointment-bot)](https://twitter.com/intent/tweet?text=Check%20this%20out%20&url=https%3A%2F%2Fgithub.com%2Franjan-mohanty%2Fvfs-appointment-bot)

This Python script(**vfs-appointment-bot**) automates checking for appointments at VFS Global portal in a specified country.

## Installation

The `vfs-appointment-bot` script can be installed using two methods:

### 1. Using pip

It is the preferred method for installing `vfs-appointment-bot`. Here's how to do it:

1. **Create a virtual environment (Recommended):**

   ```bash
   python3 -m venv venv
   ```

   This creates a virtual environment named `venv` to isolate project dependencies from your system-wide Python installation (**recommended**).

2. **Activate the virtual environment:**

   **Linux/macOS:**

   ```bash
   source venv/bin/activate
   ```

   **Windows:**

   ```bash
   venv\Scripts\activate
   ```

3. **Install using pip:**

   ```bash
   pip install vfs-appointment-bot
   ```

   This will download and install the `vfs-appointment-bot` package and its dependencies into your Python environment.

### 2. Manual Installation

For an alternative installation method, clone the source code from the project repository and install it manually.

1. **Clone the repository:**

   ```bash
   git clone https://github.com/ranjan-mohanty/vfs-appointment-bot
   ```

2. **Navigate to the project directory:**

   ```bash
   cd vfs-appointment-bot
   ```

3. **Create a virtual environment (Recommended):**

   ```bash
   python3 -m venv venv
   ```

   This creates a virtual environment named `venv` to isolate project dependencies from your system-wide Python installation (**recommended**).

4. **Activate the virtual environment:**

   **Linux/macOS:**

   ```bash
   source venv/bin/activate
   ```

   **Windows:**

   ```bash
   venv\Scripts\activate
   ```

5. **Install dependencies:**

   ```bash
   pip install poetry
   poetry install
   ```

6. **Install playwright dependencies:**

   ```bash
   playwright install
   ```

## Configuration

1. Download the [`config/config.ini`](https://raw.githubusercontent.com/ranjan-mohanty/vfs-appointment-bot/main/config/config.ini) template.

   ```bash
   curl -L https://raw.githubusercontent.com/ranjan-mohanty/vfs-appointment-bot/main/config/config.ini -o config.ini
   ```

2. Update the vfs credentials and notification channel preferences. See the [Notification Channels](#notification-channels) section for details on configuring email, Twilio, and Telegram notifications.
3. Export the path of the config file to the environment variable `VFS_BOT_CONFIG_PATH`

   ```bash
   export VFS_BOT_CONFIG_PATH=<your-config-path>/config.ini
   ```

**If you installed the script by cloning the repository (manual installation)**, you can directly edit the values in `config/config.ini`.

## Usage

1. **Command-Line Argument:**

   The script requires the source and destination country code ([as per ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements)) to be provided as a command-line argument using the `-sc` or `--source-country-code` and `-dc` or `--destination-country-code` option.

2. **Running the Script:**

   There are two ways to provide required appointment details:

   - **Responding to User Prompts (recommended):**

     ```bash
     vfs-appointment-bot -sc IN -dc DE
     ```

     The script will prompt you to enter the required apponitment parameters for the specified country.

   - **Using `-ap` or `--appointment-params`:**

     Specify appointment details in a comma-separated (**not space-separated**) key-value format:

     ```bash
     vfs-appointment-bot -sc IN -dc DE -ap visa_center=X,visa_category=Y,visa_sub_category=Z
     ```

   The script will then connect to the VFS Global website for the specified country, search for available appointments using the provided or entered parameters, and potentially send notifications (depending on your configuration).

## Notification Channels

It currently supports three notification channels to keep you informed about appointment availability:

- **Email:** Sends notifications via a Gmail account.
- **Twilio (SMS & Voice Call):** Enables alerts through text messages and phone calls using Twilio's services.
- **Telegram:** Sends notifications directly to your Telegram account through a bot.

**Configuring Notifications:**

**Email:**

1. **Email Account:** You'll need a **Gmail account** for sending notifications.
2. **App Password:** Generate an app password for your Gmail account instead of your regular password. Refer to Google's guide for generating app passwords: [https://support.google.com/accounts/answer/185833?hl=en](https://support.google.com/accounts/answer/185833?hl=en).
3. **Configuration File:** Update your application's configuration file (`config.ini`) with the following details:

   - **`email` (Required):** Your Gmail address.
   - **`password` (Required):** Your generated Gmail app password.

**Twilio:**

1. **Create a Twilio Account (if needed):** Sign up for a free Twilio account at [https://www.twilio.com/en-us](https://www.twilio.com/en-us) to obtain account credentials and phone numbers.
2. **Retrieve Credentials:** Locate your account SID, auth token, and phone numbers within your Twilio account dashboard.
3. **Configuration File:** Update your application's configuration file (`config.ini`) with:

   - `auth_token` (Required): Your Twilio auth token
   - `account_sid` (Required): Your Twilio account SID
   - `sms_enabled` (Optional): Enables SMS notifications (default: True)
   - `call_enabled` (Optional): Enables voice call notifications (default: False)
   - `url` (Optional): Twilio API URL (Only needed if call is enabled)
   - `to_num` (Required): Recipient phone number for notifications
   - `from_num` (Required): Twilio phone number you'll use for sending messages

**Telegram:**

1. **Create a Telegram Bot:** Visit [https://telegram.me/BotFather](https://telegram.me/BotFather) to create a Telegram bot. Follow the on-screen instructions to obtain your bot's token.
2. **Configuration File:** Update your application's configuration file (`config.ini`) with:

   - **`bot_token` (Required):** Your Telegram bot token obtained from BotFather.
   - **`chat_id` (Optional):** The specific Telegram chat ID where you want to receive notifications. If omitted, the bot will send notifications to the chat where it was messaged from. To find your chat ID, you can create a group chat with just yourself and then use the `/my_id` command within the bot.

## Supported Countries and Appointment Parameters

The following table lists currently supported countries and their corresponding appointment parameters:

| Country                    | Appointment Parameters                                      |
| -------------------------- | ----------------------------------------------------------- |
| India(IN) - Germany(DE)    | visa_category, visa_sub_category, visa_center               |
| Iraq(IQ) - Germany(DE)     | visa_category, visa_sub_category, visa_center               |
| Morocco(MA) - Italy(IT)    | visa_category, visa_sub_category, visa_center, payment_mode |
| Azerbaijan(AZ) - Italy(IT) | visa_category, visa_sub_category, visa_center               |

**Notes:**

- Appointment parameters might vary depending on the specific country and visa type. Always consult VFS Global's website for the latest information.

## Known Issues

**1. Login Failures After Frequent Requests:**  
If the bot makes login requests to the VFS website too frequently, the VFS system might temporarily block your access due to suspected automation. This can lead to login failures.

- **Workaround:**
  - **Reduce request frequency:** Consider increasing the delay between bot runs to avoid triggering VFS's blocking mechanisms. You can adjust the interval in the configuration or code.
  - **Retry after 2 hours:** If you encounter a login failure, wait for at least 2 hours before retrying. The VFS block should expire within this timeframe.

**2. Occasional Captcha Verification:**  
The VFS website requires a CAPTCHA verification step during login. Currently, the bot does not have a built-in CAPTCHA solver.

- **Workaround:**
  - **Wait and Retry:** Sometimes, CAPTCHAs appear due to temporary website issues. Wait for a while and try again later.
  - **Retry in another browser:** CAPTCHAs are often solved automatically in the Firefox browser. If it still fails, retry the login process in another browser by setting `browser_type` to `"chromium" or "webkit"` in your `config.ini` file.

**Note:** We are constantly working to improve the bot's functionality. Future updates might include integrated CAPTCHA solving capabilities.

## Extending Country Support

This script is currently designed to work with the VFS Global website for Germany. It might be possible to extend support for other countries by modifying the script to handle potential variations in website structure and parameter requirements across different VFS Global country pages.

## Contributing

We welcome contributions from the community to improve this project! Here's how you can get involved:

- **Report issues:** If you encounter any bugs or problems with the script, please create an issue on the project's repository.
- **Suggest improvements:** Do you have ideas for making the script more user-friendly or feature-rich? Feel free to create an issue or pull request on the repository.
- **Submit pull requests:** If you've made code changes that you think would benefit the project, create a pull request on the repository. Please follow any contribution guidelines outlined in a CONTRIBUTING.md file.

## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=ranjan-mohanty/vfs-appointment-bot&type=Date)](https://star-history.com/#ranjan-mohanty/vfs-appointment-bot&Date)

## Disclaimer

This script is provided as-is and is not affiliated with VFS Global. It's your responsibility to ensure you're complying with VFS Global's terms and conditions when using this script. Be aware that website structures and appointment availability mechanisms might change over time.


================================================
FILE: SECURITY.md
================================================
## Security Policy for VFS Appointment Bot

This document outlines the security policy for the VFS Appointment Bot project.

**1. Reporting Vulnerabilities:**

We appreciate your help in keeping this project secure. If you discover a security vulnerability, please report it responsibly by following these steps:

**1.1 Public Reporting:**

- If the vulnerability can be disclosed publicly without compromising security, you can create a public issue report on the project's GitHub repository.

**1.2 Private Reporting:**

- **We have enabled private vulnerability reporting on GitHub.** For vulnerabilities that should be kept confidential until a fix is released, please follow the steps outlined in the [GitHub documentation](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability)

- **Detailed Description:** Provide a detailed description of the vulnerability, including steps to reproduce it and potential impact.
- **Confidentiality:** Keep the vulnerability confidential until a fix is released to prevent exploitation.

We will acknowledge your report and work on a fix with the following goals:

- **Timely Response:** We will address reported vulnerabilities as quickly as possible.
- **Transparency:** We will keep you informed of the progress towards a fix and its estimated release date.
- **Fix Release:** We will release a fix for the vulnerability in a timely manner.

**2. Secure Coding Practices:**

The script development follows best practices for secure coding to minimize vulnerabilities. These practices include:

- **Input Validation:** User input is sanitized to prevent injection attacks (e.g., SQL injection, XSS).
- **Dependency Management:** Dependencies are kept up-to-date to address known vulnerabilities in external libraries.
- **Secret Handling:** Sensitive information (if any) is not stored in plain text.

**3. Supported Versions:**

We will only provide security fixes for the most recent versions of the bot. Users are encouraged to stay up-to-date with the latest releases to benefit from the latest security improvements.

**4. Disclaimer:**

While we strive to maintain the security of this script through development practices, it's provided as-is and we cannot guarantee that it is completely free of vulnerabilities. Users are encouraged to exercise caution when using any automated tools that interact with websites.

**5. Responsible Use:**

This script is intended for automating appointment checks on a public website. Users are responsible for using the script in a compliant and ethical manner, respecting robots.txt and terms of service of VFS Global's website.

**6. Reporting Abuses:**

If you suspect any misuse of this script for malicious purposes, please contact the project maintainer immediately.

We appreciate your cooperation in using this script responsibly!


================================================
FILE: pyproject.toml
================================================
# Project metadata
[tool.poetry]
name = "vfs-appointment-bot"
version = "1.1.1"
description = "VFS Appointment Bot - This script automates checking for appointments at VFS Global offices in a specified country."
authors = ["Ranjan Mohanty <ranjan@duck.com>"]
license = "MIT"
readme = "README.md"
keywords = ["vfs", "vfs-bot", "vfs-appointment-bot", "visa-appointment-bot"]

# URLs
[tool.poetry.urls]
repository = "https://github.com/ranjan-mohanty/vfs-appointment-bot/blob/main/README.md"
homepage = "https://github.com/ranjan-mohanty/vfs-appointment-bot/blob/main"

# Build System
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

# Dependencies
[tool.poetry.dependencies]
python = "^3.9"
playwright = "^1.43.0"
playwright-stealth = "^1.0.6"
twilio = "^9.0.4"
tqdm = "^4.66.2"

[tool.poetry.dev-dependencies]
flake8 = "^7.0.0"
black = "^24.4.2"

#Entry points
[tool.poetry.scripts]
vfs-appointment-bot = "vfs_appointment_bot.main:main"


================================================
FILE: vfs_appointment_bot/main.py
================================================
import argparse
import logging
import sys
from typing import Dict

from vfs_appointment_bot.utils.config_reader import get_config_value, initialize_config
from vfs_appointment_bot.utils.timer import countdown
from vfs_appointment_bot.vfs_bot.vfs_bot import LoginError
from vfs_appointment_bot.vfs_bot.vfs_bot_factory import (
    UnsupportedCountryError,
    get_vfs_bot,
)


class KeyValueAction(argparse.Action):
    """Custom action class for parsing appointment parameters.

    This class handles parsing comma-separated key-value pairs provided through
    the `--appointment-params` argument. It ensures the format is valid (key=value)
    and stores the parsed parameters as a dictionary.
    """

    def __call__(self, parser, namespace, values, option_string=None):
        try:
            appointment_params: Dict[str, str] = {
                key.strip(): value.strip()
                for key, value in (item.split("=") for item in values.split(","))
            }
            setattr(namespace, "appointment_params", appointment_params)
        except ValueError:
            parser.error(
                f"Invalid value format for {option_string}, use key=value pairs"
            )


def main() -> None:
    """
    Entry point for the VFS Appointment Bot.

    This function sets up logging, parses command-line arguments, and runs the VFS appointment
    checking process in a continuous loop. It catches exceptions for unsupported countries and
    unexpected errors, logging them appropriately.

    Raises:
        UnsupportedCountryError: If the provided country code is not supported by the bot.
        Exception: For any other unexpected errors encountered during execution.
    """
    initialize_logger()
    initialize_config()

    parser = argparse.ArgumentParser(
        description="VFS Appointment Bot: Checks for appointments at VFS Global"
    )
    required_args = parser.add_argument_group("required arguments")
    required_args.add_argument(
        "-sc",
        "--source-country-code",
        type=str,
        help="The ISO 3166-1 alpha-2 source country code (refer to README)",
        metavar="<country_code>",
        required=True,
    )

    required_args.add_argument(
        "-dc",
        "--destination-country-code",
        type=str,
        help="The ISO 3166-1 alpha-2 destination country code (refer to README)",
        metavar="<country_code>",
        required=True,
    )

    parser.add_argument(
        "-ap",
        "--appointment-params",
        type=str,
        default=None,
        help="Comma-separated key-value pairs for additional appointment details (refer to VFS website)",
        action=KeyValueAction,
        metavar="<key1=value1,key2=value2,...>",
    )

    args = parser.parse_args()
    source_country_code = args.source_country_code
    destination_country_code = args.destination_country_code
    try:
        while True:
            vfs_bot = get_vfs_bot(source_country_code, destination_country_code)
            appointment_found = vfs_bot.run(args)
            if appointment_found:
                break
            countdown(
                int(get_config_value("default", "interval")),
                "Next appointment check in",
            )

    except (UnsupportedCountryError, LoginError) as e:
        logging.error(e)
    except Exception as e:
        logging.exception(e)


def initialize_logger():
    file_handler = logging.FileHandler("app.log", mode="a")
    file_handler.setFormatter(
        logging.Formatter(
            "[%(asctime)s] %(levelname)s [%(filename)s:%(lineno)d] %(message)s"
        )
    )

    stream_handler = logging.StreamHandler(sys.stdout)
    stream_handler.setFormatter(logging.Formatter("[%(asctime)s] %(message)s"))
    logging.basicConfig(
        level=logging.INFO,
        format="[%(asctime)s] %(levelname)s [%(filename)s:%(lineno)d] %(message)s",
        handlers=[
            file_handler,
            stream_handler,
        ],
    )


if __name__ == "__main__":
    main()


================================================
FILE: vfs_appointment_bot/notification/email_client.py
================================================
import smtplib
import logging

from vfs_appointment_bot.notification.notification_client import NotificationClient


class EmailClient(NotificationClient):
    def __init__(self):
        """
        Initializes the email client with configuration data.

        This constructor retrieves configuration settings from the designated
        section (e.g., `"email"`) of the application configuration and
        validates them using the base class validation logic.
        """
        required_keys = ["email", "password"]
        super().__init__("email", required_keys)

    def send_notification(self, message: str) -> None:
        """
        Sends a notification message through the email channel.

        This method sends an email notification using the provided message content.
        It connects securely to the configured SMTP server (e.g., Gmail's SMTP),
        authenticates with the provided credentials, and constructs a well-formatted
        email before sending it.

        Args:
            message (str): The message content to be included in the email.
        """
        email: str = self.config.get("email")
        password: str = self.config.get("password")
        email_text = self.__construct_email_text(email, message)

        smtp_server = smtplib.SMTP_SSL("smtp.gmail.com", 465)
        smtp_server.ehlo()
        smtp_server.login(email, password)
        smtp_server.sendmail(email, email, email_text)
        smtp_server.close()
        logging.info("Email sent successfully!")

    def __construct_email_text(self, email: str, message: str) -> str:
        """
        Constructs a formatted email text with sender, receiver, subject,
        and message body.

        Args:
            message (str): The message content to be included in the email body.

        Returns:
            str: The formatted email text ready for sending.
        """
        return f"From: {email}\nTo: {email}\nSubject: VFS Appointment Bot Notification\n\n{message}"


================================================
FILE: vfs_appointment_bot/notification/notification_client.py
================================================
from abc import ABC, abstractmethod
from typing import List

from vfs_appointment_bot.utils.config_reader import get_config_section


class NotificationClient(ABC):
    """Abstract base class for notification clients.

    This class defines the common interface for notification clients used
    throughout the application. Subclasses must implement the `send_notification`
    method to provide specific notification sending functionality for their
    respective channels.
    """

    def __init__(self, config_section: str, required_config_keys: List[str]):
        """
        Initializes the client with configuration data.

        Args:
            config_section (str): The name of the configuration section
                containing client-specific settings.
            required_config_keys (list[str]): A list of required keys that must be
                present in the configuration section.
        """
        self.required_keys = required_config_keys
        self.config = get_config_section(config_section)
        self._validate_config(required_config_keys)

    @abstractmethod
    def send_notification(self, message: str) -> None:
        """
        Sends a notification message to the recipient.

        This method is abstract and must be implemented by subclasses to
        provide the specific logic for sending notifications through their
        respective channels.

        Args:
            message (str): The message content to be sent.
        """

    def _validate_config(self, required_config_keys: list[str]):
        """
        Validates the configuration of the notification client.

        This method checks if all required configuration keys are present
        and have non-null values. If any validation errors are found,
        appropriate exceptions are raised.

        Args:
            required_config_keys (list[str]): A list of required keys that must be
                present in the configuration section.
        """
        missing_keys = required_config_keys - self.config.keys()
        if missing_keys:
            raise NotificationClientConfigValidationError(
                f"Missing required configuration keys: {', '.join(missing_keys)}"
            )

        for key in self.required_keys:
            if self.config.get(key) is None:
                raise NotificationClientConfigValidationError(
                    f"Value for key '{key}' cannot be null."
                )


class NotificationClientConfigValidationError(Exception):
    """Exception raised when notification client configuration validation fails."""


class NotificationClientError(Exception):
    """Exception raised when an error occurs during notification sending."""


================================================
FILE: vfs_appointment_bot/notification/notification_client_factory.py
================================================
from vfs_appointment_bot.notification.notification_client import NotificationClient


class UnsupportedNotificationChannelError(Exception):
    """Raised when an unsupported notification channel is provided."""


def get_notification_client(channel: str) -> NotificationClient:
    """Retrieves the appropriate notification client for a given channel.

    This function creates an instance of a notification client class based on the
    provided channel string. Currently supported channels include "telegram" and
    "slack". If an unsupported channel is provided, a `ValueError` exception is
    raised.

    Args:
        channel (str): The notification channel name.

    Returns:
        NotificationClient: An instance of the `NotificationClient` sub class specific to the provided channel.

    Raises:
        UnsupportedNotificationChannelError: If the provided notification channel is not supported.
    """

    if channel == "telegram":
        from .telegram_client import TelegramClient

        return TelegramClient()
    elif channel == "slack":
        from .twilio_client import TwilioClient

        return TwilioClient()
    elif channel == "email":
        from .email_client import EmailClient

        return EmailClient()
    else:
        raise UnsupportedNotificationChannelError(
            f"Notification channel '{channel}' is not supported"
        )


================================================
FILE: vfs_appointment_bot/notification/telegram_client.py
================================================
import logging

import requests

from vfs_appointment_bot.notification.notification_client import NotificationClient


class TelegramClient(NotificationClient):
    """Concrete implementation of NotificationClient for the Telegram channel.

    This class provides functionality for sending notifications through the Telegram
    messaging platform. It inherits from the abstract `NotificationClient` class
    and implements the required `send_notification` method for Telegram-specific
    notification sending logic.
    """

    def __init__(self):
        """
        Initializes the Telegram client with configuration data.

        This constructor retrieves configuration settings from the "telegram"
        section of the application configuration and validates them using the
        base class validation logic.
        """
        required_keys = ["bot_token", "chat_id", "parse_mode"]
        super().__init__("telegram", required_keys)

    def send_notification(self, message: str) -> None:
        """
        Sends a notification message through the Telegram channel.

        This method constructs a Telegram API request URL using the retrieved
        configuration settings (bot token, chat ID, and parse mode) and sends a
        GET request to the Telegram API with the message content. The response
        from the Telegram API is logged for debugging purposes.

        Args:
            message (str): The message content to be sent as a Telegram notification.
        """
        bot_token: str = self.config.get("bot_token")
        chat_id: str = self.config.get("chat_id")
        parse_mode: str = self.config.get("parse_mode")

        url = (
            f"https://api.telegram.org/bot{bot_token}/sendMessage?"
            + f"chat_id={chat_id}&parse_mode={parse_mode}&text={message}"
        )
        requests.get(url, timeout=3000).json()
        logging.info("Telegram message sent successfully!")


================================================
FILE: vfs_appointment_bot/notification/twilio_client.py
================================================
import logging
from typing import Optional

from twilio.rest import Client

from vfs_appointment_bot.notification.notification_client import NotificationClient


class TwilioClient(NotificationClient):
    """Concrete implementation of NotificationClient for the Twilio channel.

    This class provides functionality for sending notifications through the Twilio
    communication platform. It inherits from the abstract `NotificationClient` class
    and implements the required `send_notification` method for Twilio-specific
    notification sending logic, including SMS messages and calls (if enabled).
    """

    def __init__(self):
        """
        Initializes the Twilio client with configuration data.

        This constructor retrieves configuration settings from the "twilio"
        section of the application configuration and validates them using the
        base class validation logic.
        """
        required_config_keys = [
            "to_num",
            "from_num",
            "account_sid",
            "auth_token",
            "url",
            "call_enabled",
        ]
        super().__init__("twilio", required_config_keys)

    def send_notification(self, message: str) -> None:
        """
        Sends a notification message through the Twilio channel.

        This method sends an SMS message using the provided message content.
        Optionally, if the "call_enabled" flag is set to True in the
        configuration, it also initiates a call to the specified phone number
        using a pre-recorded URL (provided by the "url" configuration option).

        Args:
            message (str): The message content to be sent as a Twilio SMS.
        """
        url: Optional[str] = self.config.get("url")
        auth_token: str = self.config.get("auth_token")
        account_sid: str = self.config.get("account_sid")
        to_num: str = self.config.get("to_num")
        from_num: str = self.config.get("from_num")
        call_enabled: bool = self.config.get("call_enabled", False)

        self.__send_message(message, auth_token, account_sid, to_num, from_num)

        if call_enabled:
            self.__call(url, auth_token, account_sid, to_num, from_num)

    def __send_message(
        self,
        message: str,
        auth_token: str,
        account_sid: str,
        to_num: str,
        from_num: str,
    ) -> None:
        """
        Sends an SMS message using the Twilio API.

        This private helper method creates a Twilio client instance and uses it
        to send an SMS message with the provided content to the specified recipient
        phone number.

        Args:
            message (str): The message content to be sent.
            auth_token (str): The Twilio account authentication token.
            account_sid (str): The Twilio account SID.
            to_num (str): The recipient phone number.
            from_num (str): The Twilio phone number used to send the message.
        """
        client = Client(account_sid, auth_token)
        client.messages.create(to=to_num, from_=from_num, body=message)
        logging.info("Message sent successfully!")

    def __call(
        self,
        url: Optional[str],
        auth_token: str,
        account_sid: str,
        to_num: str,
        from_num: str,
    ) -> None:
        """
        Initiates a call using the Twilio API (if URL is provided).

        This private helper method creates a Twilio client instance and uses it
        to initiate a call to the specified recipient phone number, using a
        pre-recorded URL for the call content (if provided in the configuration).

        Args:
            url (Optional[str]): The URL for the pre-recorded call content.
            auth_token (str): The Twilio account authentication token.
            account_sid (str): The Twilio account SID.
            to_num (str): The recipient phone number.
            from_num (str): The Twilio phone number used to initiate the call.
        """
        if url:
            client = Client(account_sid, auth_token)
            client.calls.create(from_=from_num, to=to_num, url=url)
            logging.info("Call request sent successfully!")
        else:
            logging.warning("No URL provided for call request!")


================================================
FILE: vfs_appointment_bot/utils/config_reader.py
================================================
import os
from configparser import ConfigParser
from typing import Dict

_config: ConfigParser = None


def initialize_config(config_dir="config"):
    """
    Reads all INI configuration files in a directory and caches the result.
    Also reads user config from `VFS_BOT_CONFIG_PATH` env var (if set)

    Args:
        config_dir: The directory containing configuration files (default: "config").
    """
    global _config
    if not _config:
        _config = ConfigParser()
        for entry in os.scandir(config_dir):
            if entry.is_file() and entry.name.endswith(".ini"):
                config_file_path = os.path.join(config_dir, entry.name)
                _config.read(config_file_path)

    # Read user defined config file
    user_config_path = os.environ.get("VFS_BOT_CONFIG_PATH")
    if user_config_path:
        _config.read(user_config_path)


def get_config_section(section: str, default: Dict = None) -> Dict:
    """
    Get a configuration section as a dictionary.

    Args:
        section: The name of the section to retrieve.
        default: A dictionary containing default values for the section (optional).

    Returns:
        A dictionary containing the configuration for the specified section,
        or the provided default dictionary if the section is not found.
    """
    if _config.has_section(section):
        return dict(_config[section])
    else:
        return default or {}


def get_config_value(section: str, key: str, default: str = None) -> str:
    """
    Get a specific configuration value.

    Args:
        section: The name of the section containing the value.
        key: The name of the key to retrieve.
        default: The default value to return if the section or key is not found (optional).

    Returns:
        The value associated with the given key within the specified section,
        or the provided default value if the section or key does not exist.
    """
    if _config.has_section(section) and _config.has_option(section, key):
        return _config[section][key]
    else:
        return default


================================================
FILE: vfs_appointment_bot/utils/date_utils.py
================================================
import re


def extract_date_from_string(text):
    pattern = r"(\d{4}-\d{2}-\d{2}|\d{2}-\d{2}-\d{4}|\d{2}-\d{2}-\d{2})"
    match = re.search(pattern, text)
    if match:
        return match.group()
    else:
        return None


================================================
FILE: vfs_appointment_bot/utils/timer.py
================================================
import time

from tqdm import tqdm


def countdown(t: int, message="Countdown", unit="seconds"):
    """
    Implements a countdown timer

    Args:
        t (int): The initial countdown time in the specified unit (e.g., seconds, minutes).
        message (str, optional): The message to display during the countdown.
                                Defaults to "Countdown".
        unit (str, optional): The unit of time for the countdown. Defaults to "seconds".
    """
    with tqdm(
        total=t, desc=message, unit=unit, bar_format="{desc}: {remaining}"
    ) as pbar:
        for _ in range(t):
            time.sleep(1)
            pbar.update(1)


================================================
FILE: vfs_appointment_bot/vfs_bot/vfs_bot.py
================================================
import argparse
import logging
from abc import ABC, abstractmethod
from typing import Dict, List

import playwright
from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync

from vfs_appointment_bot.utils.config_reader import get_config_value
from vfs_appointment_bot.notification.notification_client_factory import (
    get_notification_client,
)


class LoginError(Exception):
    """Exception raised when login fails."""


class VfsBot(ABC):
    """
    Abstract base class for VfsBot

    Provides common functionalities like login, pre-login steps, appointment checking, and notification.
    Subclasses are responsible for implementing country-specific login and appointment checking logic.
    """

    def __init__(self):
        """
        Initializes a VfsBot instance for a specific country.

        """
        self.source_country_code = None
        self.destination_country_code = None
        self.appointment_param_keys: List[str] = []

    def run(self, args: argparse.Namespace = None) -> bool:
        """
        Starts the VFS bot for appointment checking and notification.

        This method reads configuration values, performs login, checks for
        appointments based on provided arguments, and sends notifications if
        appointments are found.

        Args:
            args (argparse.Namespace, optional): Namespace object containing parsed
                command-line arguments. Defaults to None.

        Returns:
            bool: True if appointments were found, False otherwise.
        """

        logging.info(
            f"Starting VFS Bot for {self.source_country_code.upper()}-{self.destination_country_code.upper()}"
        )

        # Configuration values
        try:
            browser_type = get_config_value("browser", "type", "firefox")
            headless_mode = get_config_value("browser", "headless", "True")
            url_key = self.source_country_code + "-" + self.destination_country_code
            vfs_url = get_config_value("vfs-url", url_key)
        except KeyError as e:
            logging.error(f"Missing configuration value: {e}")
            return

        email_id = get_config_value("vfs-credential", "email")
        password = get_config_value("vfs-credential", "password")

        appointment_params = self.get_appointment_params(args)

        # Launch browser and perform actions
        with sync_playwright() as p:
            browser = getattr(p, browser_type).launch(
                headless=headless_mode in ("True", "true")
            )
            page = browser.new_page()
            stealth_sync(page)

            page.goto(vfs_url)
            self.pre_login_steps(page)

            try:
                self.login(page, email_id, password)
                logging.info("Logged in successfully")
            except Exception:
                browser.close()
                raise LoginError(
                    "\033[1;31mLogin failed. "
                    + "Please verify your username and password by logging in to the browser and try again.\033[0m"
                )

            logging.info(f"Checking appointments for {appointment_params}")
            appointment_found = False
            try:
                dates = self.check_for_appontment(page, appointment_params)
                if dates:
                    # Log successful appointment finding
                    logging.info(
                        f"\033[1;32mFound appointments on: {', '.join(dates)} \033[0m"
                    )
                    self.notify_appointment(appointment_params, dates)
                    appointment_found = True
                else:
                    # Log no appointments found
                    logging.info(
                        "\033[1;33mNo appointments found for the specified criteria.\033[0m"
                    )
            except Exception as e:
                logging.error(f"Appointment check failed: {e}")
            browser.close()
            return appointment_found

    def get_appointment_params(self, args: argparse.Namespace) -> Dict[str, str]:
        """
        Collects appointment parameters from command-line arguments or user input.

        This method iterates through pre-defined `appointment_param_keys` (replace
        with relevant keys) and retrieves values either from provided arguments
        or prompts the user for input if values are missing.

        Args:
            args (argparse.Namespace): Namespace object containing parsed command-line arguments.

        Returns:
            Dict[str, str]: A dictionary containing appointment parameters.
        """
        appointment_params = {}
        for key in self.appointment_param_keys:
            if (
                getattr(args, "appointment_params") is not None
                and args.appointment_params[key] is not None
            ):
                appointment_params[key] = args.appointment_params[key]
            else:
                key_name = key.replace("_", " ")
                appointment_params[key] = input(f"Enter the {key_name}: ")
        return appointment_params

    def notify_appointment(self, appointment_params: Dict[str, str], dates: List[str]):
        """
        Sends appointment dates notification to the user.

        This method is responsible for notifying the appointment dates to the user configured channels

        Args:
            dates (List[str]): A list of appointment dates.
            appointment_params (Dict[str, str]): A dictionary containing appointment search criteria.
        """
        message = f"Found appointment(s) for {', '.join(appointment_params.values())} on {', '.join(dates)}"
        channels = get_config_value("notification", "channels")
        if len(channels) == 0:
            logging.warning(
                "No notification channels configured. Skipping notification."
            )
            return

        for channel in channels.split(","):
            client = get_notification_client(channel)
            try:
                client.send_notification(message)
            except Exception:
                logging.error(f"Failed to send {channel} notification")

    @abstractmethod
    def login(
        self, page: playwright.sync_api.Page, email_id: str, password: str
    ) -> None:
        """
        Performs login steps specific to the VFS website for the bot's country.

        This abstract method needs to be implemented by subclasses to handle
        country-specific login procedures (e.g., filling login form elements, handling
        CAPTCHAs). It should interact with the Playwright `page` object to achieve
        login functionality.

        Args:
            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.
            email_id (str): The user's email address for VFS login.
            password (str): The user's password for VFS login.

        Raises:
            Exception: If login fails due to unexpected errors.
        """
        raise NotImplementedError("Subclasses must implement login logic")

    @abstractmethod
    def pre_login_steps(self, page: playwright.sync_api.Page) -> None:
        """
        Performs any pre-login steps required by the VFS website for the bot's country.

        This abstract method allows subclasses to implement country-specific actions
        that need to be done before login (e.g., cookie acceptance, language selection).
        It should interact with the Playwright `page` object to perform these actions.

        Args:
            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.
        """

    @abstractmethod
    def check_for_appontment(
        self, page: playwright.sync_api.Page, appointment_params: Dict[str, str]
    ) -> List[str]:
        """
        Checks for appointments based on provided parameters on the VFS website.

        This abstract method needs to be implemented by subclasses to interact with
        the VFS website and search for appointments based on the given `appointment_params`
        dictionary. It should use the Playwright `page` object to navigate the website
        and extract appointment dates.

        Args:
            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.
            appointment_params (Dict[str, str]): A dictionary containing appointment search criteria.

        Returns:
            List[str]: A list of available appointment dates (empty list if none found).
        """
        raise NotImplementedError(
            "Subclasses must implement appointment checking logic"
        )


================================================
FILE: vfs_appointment_bot/vfs_bot/vfs_bot_de.py
================================================
import logging
from typing import Dict, List, Optional

from playwright.sync_api import Page

from vfs_appointment_bot.utils.date_utils import extract_date_from_string
from vfs_appointment_bot.vfs_bot.vfs_bot import VfsBot


class VfsBotDe(VfsBot):
    """Concrete implementation of VfsBot for Germany (DE).

    This class inherits from the base `VfsBot` class and implements
    country-specific logic for interacting with the VFS website for Germany.
    It overrides the following methods to handle German website specifics:

    - `login`: Fills the login form elements with email and password.
    - `pre_login_steps`: Rejects all cookie policies if presented.
    - `check_for_appontment`: Performs appointment search based on provided
        parameters and extracts available dates from the website.
    """

    def __init__(self, source_country_code: str):
        """
        Initializes a VfsBotDe instance for Germany.

        This constructor sets the source country code and the destination country
        code "de"(Germany). It also defines appointment parameter keys specific
        to the destination country's VFS website.

        Args:
            source_country_code (str): The country code where you're applying from.
        """
        super().__init__()
        self.source_country_code = source_country_code
        self.destination_country_code = "DE"
        self.appointment_param_keys = [
            "visa_center",
            "visa_category",
            "visa_sub_category",
        ]

    def login(self, page: Page, email_id: str, password: str) -> None:
        """
        Performs login steps specific to the German VFS website.

        This method fills the email and password input fields on the login form
        and clicks the "Sign In" button. It raises an exception if the login fails
        (e.g., if the "Start New Booking" button is not found after login).

        Args:
            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.
            email_id (str): The user's email address for VFS login.
            password (str): The user's password for VFS login.

        Raises:
            Exception: If login fails due to unexpected errors or missing "Start New Booking" button.
        """
        email_input = page.locator("#mat-input-0")
        password_input = page.locator("#mat-input-1")

        email_input.fill(email_id)
        password_input.fill(password)

        page.get_by_role("button", name="Sign In").click()
        page.wait_for_selector("role=button >> text=Start New Booking")

    def pre_login_steps(self, page: Page) -> None:
        """
        Performs pre-login steps specific to the German VFS website.

        This method checks for a "Reject All" button for cookie policies and
        clicks it if found.

        Args:
            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.
        """
        policies_reject_button = page.get_by_role("button", name="Reject All")
        if policies_reject_button is not None:
            policies_reject_button.click()
            logging.debug("Rejected all cookie policies")

    def check_for_appontment(
        self, page: Page, appointment_params: Dict[str, str]
    ) -> Optional[List[str]]:
        """
        Checks for appointments on the German VFS website based on provided parameters.

        This method clicks the "Start New Booking" button, selects the specified
        visa center, category, and subcategory based on the `appointment_params`
        dictionary. It then extracts the available appointment dates from the
        website and returns them as a list. If no appointments are found, it
        returns None.

        Args:
            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.
            appointment_params (Dict[str, str]): A dictionary containing appointment search criteria.

        Returns:
            Optional[List[str]]: A list of available appointment dates (empty list if none found),
                including a timestamp of the check, or None if no appointments found.
        """
        page.get_by_role("button", name="Start New Booking").click()

        # Select Visa Centre

        visa_centre_dropdown = page.wait_for_selector("mat-form-field")
        visa_centre_dropdown.click()
        visa_centre_dropdown_option = page.wait_for_selector(
            f'mat-option:has-text("{appointment_params.get("visa_center")}")'
        )
        visa_centre_dropdown_option.click()

        # Select Visa Category
        visa_category_dropdown = page.query_selector_all("mat-form-field")[1]
        visa_category_dropdown.click()
        visa_category_dropdown_option = page.wait_for_selector(
            f'mat-option:has-text("{appointment_params.get("visa_category")}")'
        )
        visa_category_dropdown_option.click()

        # Select Subcategory
        visa_subcategory_dropdown = page.query_selector_all("mat-form-field")[2]
        visa_subcategory_dropdown.click()
        visa_subcategory_dropdown_option = page.wait_for_selector(
            f'mat-option:has-text("{appointment_params.get("visa_sub_category")}")'
        )
        visa_subcategory_dropdown_option.click()

        try:
            page.wait_for_selector("div.alert")
            appointment_date_elements = page.query_selector_all("div.alert")
            appointment_dates = []
            for appointment_date_element in appointment_date_elements:
                appointment_date_text = appointment_date_element.text_content()
                appointment_date = extract_date_from_string(appointment_date_text)
                if appointment_date is not None and len(appointment_date) > 0:
                    appointment_dates.append(appointment_date)
            return appointment_dates
        except Exception:
            return None

        return None


================================================
FILE: vfs_appointment_bot/vfs_bot/vfs_bot_factory.py
================================================
from vfs_appointment_bot.vfs_bot.vfs_bot import VfsBot


class UnsupportedCountryError(Exception):
    """Raised when an unsupported country code is provided."""


def get_vfs_bot(source_country_code: str, destination_country_code: str) -> VfsBot:
    """Retrieves the appropriate VfsBot class for a given country.

    This function searches for a matching subclass of `VfsBot` based on the
    provided destination country code (ISO 3166-1 alpha-2).
    If no matching class is found, an `UnsupportedCountryError` exception is raised.

    Args:
        source_country_code (str): The ISO 3166-1 alpha-2 country code where you're applying from.
        destination_country_code (str): The ISO 3166-1 alpha-2 country code where the appointment is needed.

    Returns:
        VfsBot: An instance of the `VfsBot` subclass specific to the provided country.

    Raises:
        UnsupportedCountryError: If the provided country is not supported.
    """

    country_lower = destination_country_code

    if country_lower == "DE":
        from .vfs_bot_de import VfsBotDe

        return VfsBotDe(source_country_code)
    elif country_lower == "IT":
        from .vfs_bot_it import VfsBotIt

        return VfsBotIt(source_country_code)
    else:
        raise UnsupportedCountryError(
            f"Country {destination_country_code} is not supported"
        )


================================================
FILE: vfs_appointment_bot/vfs_bot/vfs_bot_it.py
================================================
import logging
from typing import Dict, List, Optional

from playwright.sync_api import Page

from vfs_appointment_bot.utils.date_utils import extract_date_from_string
from vfs_appointment_bot.vfs_bot.vfs_bot import VfsBot


class VfsBotIt(VfsBot):
    """Concrete implementation of VfsBot for Italy (IT).

    This class inherits from the base `VfsBot` class and implements
    country-specific logic for interacting with the VFS website for Italy.
    It overrides the following methods to handle Italy website specifics:

    - `login`: Fills the login form elements with email and password.
    - `pre_login_steps`: Rejects all cookie policies if presented.
    - `check_for_appontment`: Performs appointment search based on provided
        parameters and extracts available dates from the website.
    """

    def __init__(self, source_country_code: str):
        """
        Initializes a VfsBotIt instance for Italy.

        This constructor sets the source country code and the destination country
        code "it"(Italy). It also defines appointment parameter keys specific
        to the destination country's VFS website.

        Args:
            source_country_code (str): The country code where you're applying from.
        """
        super().__init__()
        self.source_country_code = source_country_code
        self.destination_country_code = "IT"
        self.appointment_param_keys = [
            "visa_center",
            "visa_category",
            "visa_sub_category",
        ]

        if self.source_country_code == "MA":
            self.appointment_param_keys.append("payment_mode")

    def login(self, page: Page, email_id: str, password: str) -> None:
        """
        Performs login steps specific to the Italy VFS website.

        This method fills the email and password input fields on the login form
        and clicks the "Sign In" button. It raises an exception if the login fails
        (e.g., if the "Start New Booking" button is not found after login).

        Args:
            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.
            email_id (str): The user's email address for VFS login.
            password (str): The user's password for VFS login.

        Raises:
            Exception: If login fails due to unexpected errors or missing "Start New Booking" button.
        """
        email_input = page.locator("#mat-input-0")
        password_input = page.locator("#mat-input-1")

        email_input.fill(email_id)
        password_input.fill(password)

        page.get_by_role("button", name="Sign In").click()
        page.wait_for_selector("role=button >> text=Start New Booking")

    def pre_login_steps(self, page: Page) -> None:
        """
        Performs pre-login steps specific to the Italy VFS website.

        This method checks for a "Reject All" button for cookie policies and
        clicks it if found.

        Args:
            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.
        """
        policies_reject_button = page.get_by_role("button", name="Reject All")
        if policies_reject_button is not None:
            policies_reject_button.click()
            logging.debug("Rejected all cookie policies")

    def check_for_appontment(
        self, page: Page, appointment_params: Dict[str, str]
    ) -> Optional[List[str]]:
        """
        Checks for appointments on the Italy VFS website based on provided parameters.

        This method clicks the "Start New Booking" button, selects the specified
        visa center, category, and subcategory based on the `appointment_params`
        dictionary. It then extracts the available appointment dates from the
        website and returns them as a list. If no appointments are found, it
        returns None.

        Args:
            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.
            appointment_params (Dict[str, str]): A dictionary containing appointment search criteria.

        Returns:
            Optional[List[str]]: A list of available appointment dates (empty list if none found),
                including a timestamp of the check, or None if no appointments found.
        """
        page.get_by_role("button", name="Start New Booking").click()

        # Select Visa Centre
        visa_centre_dropdown = page.wait_for_selector("mat-form-field")
        visa_centre_dropdown.click()
        visa_centre_dropdown_option = page.wait_for_selector(
            f'mat-option:has-text("{appointment_params.get("visa_center")}")'
        )
        visa_centre_dropdown_option.click()

        # Select Visa Category
        visa_category_dropdown = page.query_selector_all("mat-form-field")[1]
        visa_category_dropdown.click()
        visa_category_dropdown_option = page.wait_for_selector(
            f'mat-option:has-text("{appointment_params.get("visa_category")}")'
        )
        visa_category_dropdown_option.click()

        # Select Subcategory
        visa_subcategory_dropdown = page.query_selector_all("mat-form-field")[2]
        visa_subcategory_dropdown.click()
        visa_subcategory_dropdown_option = page.wait_for_selector(
            f'mat-option:has-text("{appointment_params.get("visa_sub_category")}")'
        )
        visa_subcategory_dropdown_option.click()

        if self.source_country_code == "MA":
            # Select Payment Mode
            payment_mode_dropdown = page.query_selector_all("mat-form-field")[3]
            payment_mode_dropdown.click()
            payment_mode_dropdown_option = page.wait_for_selector(
                f'mat-option:has-text("{appointment_params.get("payment_mode")}")'
            )
            payment_mode_dropdown_option.click()

        try:
            page.wait_for_selector("div.alert")
            appointment_date_elements = page.query_selector_all("div.alert")
            appointment_dates = []
            for appointment_date_element in appointment_date_elements:
                appointment_date_text = appointment_date_element.text_content()
                appointment_date = extract_date_from_string(appointment_date_text)
                if appointment_date is not None and len(appointment_date) > 0:
                    appointment_dates.append(appointment_date)
            return appointment_dates
        except Exception:
            return None

        return None
Download .txt
gitextract_xbbv28z9/

├── .flake8
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── dependabot.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── build.yml
│       ├── codeql.yml
│       ├── dependency-review.yml
│       ├── publish-testpypi.yml
│       ├── publish.yml
│       └── scorecard.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── pyproject.toml
└── vfs_appointment_bot/
    ├── main.py
    ├── notification/
    │   ├── email_client.py
    │   ├── notification_client.py
    │   ├── notification_client_factory.py
    │   ├── telegram_client.py
    │   └── twilio_client.py
    ├── utils/
    │   ├── config_reader.py
    │   ├── date_utils.py
    │   └── timer.py
    └── vfs_bot/
        ├── vfs_bot.py
        ├── vfs_bot_de.py
        ├── vfs_bot_factory.py
        └── vfs_bot_it.py
Download .txt
SYMBOL INDEX (50 symbols across 13 files)

FILE: vfs_appointment_bot/main.py
  class KeyValueAction (line 15) | class KeyValueAction(argparse.Action):
    method __call__ (line 23) | def __call__(self, parser, namespace, values, option_string=None):
  function main (line 36) | def main() -> None:
  function initialize_logger (line 103) | def initialize_logger():

FILE: vfs_appointment_bot/notification/email_client.py
  class EmailClient (line 7) | class EmailClient(NotificationClient):
    method __init__ (line 8) | def __init__(self):
    method send_notification (line 19) | def send_notification(self, message: str) -> None:
    method __construct_email_text (line 42) | def __construct_email_text(self, email: str, message: str) -> str:

FILE: vfs_appointment_bot/notification/notification_client.py
  class NotificationClient (line 7) | class NotificationClient(ABC):
    method __init__ (line 16) | def __init__(self, config_section: str, required_config_keys: List[str]):
    method send_notification (line 31) | def send_notification(self, message: str) -> None:
    method _validate_config (line 43) | def _validate_config(self, required_config_keys: list[str]):
  class NotificationClientConfigValidationError (line 68) | class NotificationClientConfigValidationError(Exception):
  class NotificationClientError (line 72) | class NotificationClientError(Exception):

FILE: vfs_appointment_bot/notification/notification_client_factory.py
  class UnsupportedNotificationChannelError (line 4) | class UnsupportedNotificationChannelError(Exception):
  function get_notification_client (line 8) | def get_notification_client(channel: str) -> NotificationClient:

FILE: vfs_appointment_bot/notification/telegram_client.py
  class TelegramClient (line 8) | class TelegramClient(NotificationClient):
    method __init__ (line 17) | def __init__(self):
    method send_notification (line 28) | def send_notification(self, message: str) -> None:

FILE: vfs_appointment_bot/notification/twilio_client.py
  class TwilioClient (line 9) | class TwilioClient(NotificationClient):
    method __init__ (line 18) | def __init__(self):
    method send_notification (line 36) | def send_notification(self, message: str) -> None:
    method __send_message (line 60) | def __send_message(
    method __call (line 86) | def __call(

FILE: vfs_appointment_bot/utils/config_reader.py
  function initialize_config (line 8) | def initialize_config(config_dir="config"):
  function get_config_section (line 30) | def get_config_section(section: str, default: Dict = None) -> Dict:
  function get_config_value (line 48) | def get_config_value(section: str, key: str, default: str = None) -> str:

FILE: vfs_appointment_bot/utils/date_utils.py
  function extract_date_from_string (line 4) | def extract_date_from_string(text):

FILE: vfs_appointment_bot/utils/timer.py
  function countdown (line 6) | def countdown(t: int, message="Countdown", unit="seconds"):

FILE: vfs_appointment_bot/vfs_bot/vfs_bot.py
  class LoginError (line 16) | class LoginError(Exception):
  class VfsBot (line 20) | class VfsBot(ABC):
    method __init__ (line 28) | def __init__(self):
    method run (line 37) | def run(self, args: argparse.Namespace = None) -> bool:
    method get_appointment_params (line 114) | def get_appointment_params(self, args: argparse.Namespace) -> Dict[str...
    method notify_appointment (line 140) | def notify_appointment(self, appointment_params: Dict[str, str], dates...
    method login (line 166) | def login(
    method pre_login_steps (line 188) | def pre_login_steps(self, page: playwright.sync_api.Page) -> None:
    method check_for_appontment (line 201) | def check_for_appontment(

FILE: vfs_appointment_bot/vfs_bot/vfs_bot_de.py
  class VfsBotDe (line 10) | class VfsBotDe(VfsBot):
    method __init__ (line 23) | def __init__(self, source_country_code: str):
    method login (line 43) | def login(self, page: Page, email_id: str, password: str) -> None:
    method pre_login_steps (line 68) | def pre_login_steps(self, page: Page) -> None:
    method check_for_appontment (line 83) | def check_for_appontment(

FILE: vfs_appointment_bot/vfs_bot/vfs_bot_factory.py
  class UnsupportedCountryError (line 4) | class UnsupportedCountryError(Exception):
  function get_vfs_bot (line 8) | def get_vfs_bot(source_country_code: str, destination_country_code: str)...

FILE: vfs_appointment_bot/vfs_bot/vfs_bot_it.py
  class VfsBotIt (line 10) | class VfsBotIt(VfsBot):
    method __init__ (line 23) | def __init__(self, source_country_code: str):
    method login (line 46) | def login(self, page: Page, email_id: str, password: str) -> None:
    method pre_login_steps (line 71) | def pre_login_steps(self, page: Page) -> None:
    method check_for_appontment (line 86) | def check_for_appontment(
Condensed preview — 33 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (88K chars).
[
  {
    "path": ".flake8",
    "chars": 144,
    "preview": "[flake8]\nexclude = \n    .git,\n    .github,\n    __pycache__,\n    venv,\n    dist,\n    config,\n    build\n\nmax-complexity = "
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 22,
    "preview": "github: ranjan-mohanty"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 829,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n**Describe the bu"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 594,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n**Is your feat"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 389,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"pip\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    ignore:\n "
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 1277,
    "preview": "## Pull Request Template\n\n**Thank you for contributing to the VFS appointment bot project!**\n\nTo streamline the review p"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 1002,
    "preview": "name: Build\non:\n  workflow_call:\n  push:\n    branches:\n      - \"**\"\n    tags-ignore:\n      - \"**\"\n\npermissions:\n  conten"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "chars": 2879,
    "preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
  },
  {
    "path": ".github/workflows/dependency-review.yml",
    "chars": 969,
    "preview": "# Dependency Review Action\n#\n# This Action will scan dependency manifest files that change as part of a Pull Request,\n# "
  },
  {
    "path": ".github/workflows/publish-testpypi.yml",
    "chars": 2291,
    "preview": "name: Publish - TestPyPI\non:\n  push:\n    tags:\n      - '**'\n\npermissions:\n  contents: read\n\njobs:\n  build:\n    uses: ./."
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 2137,
    "preview": "name: Publish - PyPI\non:\n  push:\n    tags-ignore:\n      - '*rc*'\n\npermissions:\n  contents: read\n\njobs:\n  build:\n    uses"
  },
  {
    "path": ".github/workflows/scorecard.yml",
    "chars": 2773,
    "preview": "# This workflow uses actions that are not certified by GitHub. They are provided\n# by a third-party and are governed by "
  },
  {
    "path": ".gitignore",
    "chars": 358,
    "preview": "# Distribution / packaging\ndist/\neggs/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\npoetry.lock\n\n# Unit test / coverage rep"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 462,
    "preview": "repos:\n- repo: https://github.com/gitleaks/gitleaks\n  rev: v8.16.3\n  hooks:\n  - id: gitleaks\n- repo: https://github.com/"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 5211,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 2327,
    "preview": "## Welcome to Amazon Product Details Scraper contributing guide\n\nThank you for investing your time in contributing to ou"
  },
  {
    "path": "LICENSE",
    "chars": 1071,
    "preview": "MIT License\n\nCopyright (c) 2022 Ranjan Mohanty\n\nPermission is hereby granted, free of charge, to any person obtaining a "
  },
  {
    "path": "README.md",
    "chars": 12410,
    "preview": "# VFS Appointment Bot\n\n[![GitHub License](https://img.shields.io/github/license/ranjan-mohanty/vfs-appointment-bot)](htt"
  },
  {
    "path": "SECURITY.md",
    "chars": 2911,
    "preview": "## Security Policy for VFS Appointment Bot\n\nThis document outlines the security policy for the VFS Appointment Bot proje"
  },
  {
    "path": "pyproject.toml",
    "chars": 972,
    "preview": "# Project metadata\n[tool.poetry]\nname = \"vfs-appointment-bot\"\nversion = \"1.1.1\"\ndescription = \"VFS Appointment Bot - Thi"
  },
  {
    "path": "vfs_appointment_bot/main.py",
    "chars": 4030,
    "preview": "import argparse\nimport logging\nimport sys\nfrom typing import Dict\n\nfrom vfs_appointment_bot.utils.config_reader import g"
  },
  {
    "path": "vfs_appointment_bot/notification/email_client.py",
    "chars": 1992,
    "preview": "import smtplib\nimport logging\n\nfrom vfs_appointment_bot.notification.notification_client import NotificationClient\n\n\ncla"
  },
  {
    "path": "vfs_appointment_bot/notification/notification_client.py",
    "chars": 2717,
    "preview": "from abc import ABC, abstractmethod\nfrom typing import List\n\nfrom vfs_appointment_bot.utils.config_reader import get_con"
  },
  {
    "path": "vfs_appointment_bot/notification/notification_client_factory.py",
    "chars": 1385,
    "preview": "from vfs_appointment_bot.notification.notification_client import NotificationClient\n\n\nclass UnsupportedNotificationChann"
  },
  {
    "path": "vfs_appointment_bot/notification/telegram_client.py",
    "chars": 1937,
    "preview": "import logging\n\nimport requests\n\nfrom vfs_appointment_bot.notification.notification_client import NotificationClient\n\n\nc"
  },
  {
    "path": "vfs_appointment_bot/notification/twilio_client.py",
    "chars": 4279,
    "preview": "import logging\nfrom typing import Optional\n\nfrom twilio.rest import Client\n\nfrom vfs_appointment_bot.notification.notifi"
  },
  {
    "path": "vfs_appointment_bot/utils/config_reader.py",
    "chars": 2087,
    "preview": "import os\nfrom configparser import ConfigParser\nfrom typing import Dict\n\n_config: ConfigParser = None\n\n\ndef initialize_c"
  },
  {
    "path": "vfs_appointment_bot/utils/date_utils.py",
    "chars": 231,
    "preview": "import re\n\n\ndef extract_date_from_string(text):\n    pattern = r\"(\\d{4}-\\d{2}-\\d{2}|\\d{2}-\\d{2}-\\d{4}|\\d{2}-\\d{2}-\\d{2})\""
  },
  {
    "path": "vfs_appointment_bot/utils/timer.py",
    "chars": 658,
    "preview": "import time\n\nfrom tqdm import tqdm\n\n\ndef countdown(t: int, message=\"Countdown\", unit=\"seconds\"):\n    \"\"\"\n    Implements "
  },
  {
    "path": "vfs_appointment_bot/vfs_bot/vfs_bot.py",
    "chars": 8711,
    "preview": "import argparse\nimport logging\nfrom abc import ABC, abstractmethod\nfrom typing import Dict, List\n\nimport playwright\nfrom"
  },
  {
    "path": "vfs_appointment_bot/vfs_bot/vfs_bot_de.py",
    "chars": 5966,
    "preview": "import logging\nfrom typing import Dict, List, Optional\n\nfrom playwright.sync_api import Page\n\nfrom vfs_appointment_bot.u"
  },
  {
    "path": "vfs_appointment_bot/vfs_bot/vfs_bot_factory.py",
    "chars": 1362,
    "preview": "from vfs_appointment_bot.vfs_bot.vfs_bot import VfsBot\n\n\nclass UnsupportedCountryError(Exception):\n    \"\"\"Raised when an"
  },
  {
    "path": "vfs_appointment_bot/vfs_bot/vfs_bot_it.py",
    "chars": 6478,
    "preview": "import logging\nfrom typing import Dict, List, Optional\n\nfrom playwright.sync_api import Page\n\nfrom vfs_appointment_bot.u"
  }
]

About this extraction

This page contains the full source code of the ranjan-mohanty/vfs-appointment-bot GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 33 files (80.9 KB), approximately 19.1k tokens, and a symbol index with 50 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!