[
  {
    "path": ".flake8",
    "content": "[flake8]\nexclude = \n    .git,\n    .github,\n    __pycache__,\n    venv,\n    dist,\n    config,\n    build\n\nmax-complexity = 10\nmax-line-length = 120"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: ranjan-mohanty"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n\n- OS: [e.g. iOS]\n- Browser [e.g. chrome, safari]\n- Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n\n- Device: [e.g. iPhone6]\n- OS: [e.g. iOS8.1]\n- Browser [e.g. stock browser, safari]\n- Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"pip\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    ignore:\n      - dependency-name: \"*\"\n        update-types: [\"version-update:semver-patch\"]\n\n  - package-ecosystem: github-actions\n    directory: /\n    schedule:\n      interval: daily\n    ignore:\n      - dependency-name: \"*\"\n        update-types: [\"version-update:semver-patch\"]\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Pull Request Template\n\n**Thank you for contributing to the VFS appointment bot project!**\n\nTo streamline the review process, please fill out the details below when creating a pull request.\n\n**Title:**\n\n- Briefly describe your changes here\n\n**Description:**\n\n- What changes did you make?\n  - Explain the purpose of your changes and how they address an issue or improve the scraper.\n  - If applicable, mention any new features you've implemented.\n  - Include steps to reproduce any bug fixes (if applicable).\n- How did you test your changes?\n  - Did you add new unit tests?\n  - Did you manually test with different scenarios?\n\n**Additional Information:**\n\n- Did you add any dependencies or external libraries?\n- Did you modify the documentation? If so, how?\n\n**Checklist:**\n\n- [ ] I followed the Coding Style Guide: [https://peps.python.org/pep-0008/](https://peps.python.org/pep-0008/).\n- [ ] I added clear and concise comments to my code.\n- [ ] I added unit tests for new features or bug fixes (if applicable).\n- [ ] I ensured my changes do not introduce regressions.\n\n**Optional:**\n\n- Issue reference (if related to an existing issue): # (issue number)\n\n**By providing these details, you'll help us review your pull request efficiently. We appreciate your contributions!**\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\non:\n  workflow_call:\n  push:\n    branches:\n      - \"**\"\n    tags-ignore:\n      - \"**\"\n\npermissions:\n  contents: read\n\njobs:\n  build:\n    name: Build distribution\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1\n        with:\n          egress-policy: audit\n\n      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n      - name: Set up Python\n        uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1\n        with:\n          python-version: \"3.x\"\n      - name: Install pypa/build\n        run: python3 -m pip install build --user\n      - name: Build a binary wheel and a source tarball\n        run: python3 -m build\n      - name: Store the distribution packages\n        uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4\n        with:\n          name: python-package-distributions\n          path: dist/\n"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [\"main\"]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [\"main\"]\n  schedule:\n    - cron: \"0 0 * * 1\"\n\npermissions:\n  contents: read\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [\"python\"]\n        # CodeQL supports [ $supported-codeql-languages ]\n        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support\n\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1\n        with:\n          egress-policy: audit\n\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n\n      # Initializes the CodeQL tools for scanning.\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5\n        with:\n          languages: ${{ matrix.language }}\n          # If you wish to specify custom queries, you can do so here or in a config file.\n          # By default, queries listed here will override any specified in a config file.\n          # Prefix the list here with \"+\" to use these queries and those in the config file.\n\n      # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n      # If this step fails, then you should remove it and run the build manually (see below)\n      - name: Autobuild\n        uses: github/codeql-action/autobuild@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5\n\n      # ℹ️ Command-line programs to run using the OS shell.\n      # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun\n\n      #   If the Autobuild fails above, remove it and uncomment the following three lines.\n      #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.\n\n      # - run: |\n      #   echo \"Run, Build Application using script\"\n      #   ./location_of_script_within_repo/buildscript.sh\n\n      - name: Perform CodeQL Analysis\n        uses: github/codeql-action/analyze@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5\n        with:\n          category: \"/language:${{matrix.language}}\"\n"
  },
  {
    "path": ".github/workflows/dependency-review.yml",
    "content": "# Dependency Review Action\n#\n# This Action will scan dependency manifest files that change as part of a Pull Request,\n# surfacing known-vulnerable versions of the packages declared or updated in the PR.\n# Once installed, if the workflow run is marked as required,\n# PRs introducing known-vulnerable packages will be blocked from merging.\n#\n# Source repository: https://github.com/actions/dependency-review-action\nname: 'Dependency Review'\non: [pull_request]\n\npermissions:\n  contents: read\n\njobs:\n  dependency-review:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1\n        with:\n          egress-policy: audit\n\n      - name: 'Checkout Repository'\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n      - name: 'Dependency Review'\n        uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4\n"
  },
  {
    "path": ".github/workflows/publish-testpypi.yml",
    "content": "name: Publish - TestPyPI\non:\n  push:\n    tags:\n      - '**'\n\npermissions:\n  contents: read\n\njobs:\n  build:\n    uses: ./.github/workflows/build.yml\n  publish-to-testpypi:\n    name: Publish to TestPyPI\n    needs:\n      - build\n    runs-on: ubuntu-latest\n\n    environment:\n      name: testpypi\n      url: https://test.pypi.org/p/vfs-appointment-bot\n\n    permissions:\n      id-token: write\n\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1\n        with:\n          egress-policy: audit\n\n      - name: Download all the dists\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8\n        with:\n          name: python-package-distributions\n          path: dist/\n      - name: Publish distribution to TestPyPI\n        uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # release/v1\n        with:\n          repository-url: https://test.pypi.org/legacy/\n  github-release:\n    if: ${{contains(github.ref, 'rc')}}\n    name: Release\n    needs:\n      - publish-to-testpypi\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: write\n      id-token: write\n\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1\n        with:\n          egress-policy: audit\n\n      - name: Download all the dists\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8\n        with:\n          name: python-package-distributions\n          path: dist/\n      - name: Sign the dists with Sigstore\n        uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0\n        with:\n          inputs: >-\n            ./dist/*.tar.gz\n            ./dist/*.whl\n      - name: Create GitHub Release\n        env:\n          GITHUB_TOKEN: ${{ github.token }}\n        run: >-\n          gh release create\n          '${{ github.ref_name }}'\n          --repo '${{ github.repository }}'\n          --prerelease\n      - name: Upload artifact signatures to GitHub Release\n        env:\n          GITHUB_TOKEN: ${{ github.token }}\n        run: >-\n          gh release upload\n          '${{ github.ref_name }}' dist/**\n          --repo '${{ github.repository }}'\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish - PyPI\non:\n  push:\n    tags-ignore:\n      - '*rc*'\n\npermissions:\n  contents: read\n\njobs:\n  build:\n    uses: ./.github/workflows/build.yml\n  publish-to-pypi:\n    name: Publish to PyPI\n    needs:\n      - build\n    runs-on: ubuntu-latest\n    environment:\n      name: pypi\n      url: https://pypi.org/p/vfs-appointment-bot\n    permissions:\n      id-token: write\n\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1\n        with:\n          egress-policy: audit\n\n      - name: Download all the dists\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8\n        with:\n          name: python-package-distributions\n          path: dist/\n      - name: Publish distribution to PyPI\n        uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # release/v1\n\n  github-release:\n    name: Release\n    needs:\n      - publish-to-pypi\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: write\n      id-token: write\n\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1\n        with:\n          egress-policy: audit\n\n      - name: Download all the dists\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8\n        with:\n          name: python-package-distributions\n          path: dist/\n      - name: Sign the dists with Sigstore\n        uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0\n        with:\n          inputs: >-\n            ./dist/*.tar.gz\n            ./dist/*.whl\n      - name: Create GitHub Release\n        env:\n          GITHUB_TOKEN: ${{ github.token }}\n        run: >-\n          gh release create\n          '${{ github.ref_name }}'\n          --repo '${{ github.repository }}'\n      - name: Upload artifact signatures to GitHub Release\n        env:\n          GITHUB_TOKEN: ${{ github.token }}\n        run: >-\n          gh release upload\n          '${{ github.ref_name }}' dist/**\n          --repo '${{ github.repository }}'\n"
  },
  {
    "path": ".github/workflows/scorecard.yml",
    "content": "# This workflow uses actions that are not certified by GitHub. They are provided\n# by a third-party and are governed by separate terms of service, privacy\n# policy, and support documentation.\n\nname: Scorecard Security\non:\n  push:\n    branches: [\"main\"]\n\n# Declare default permissions as read only.\npermissions: read-all\n\njobs:\n  analysis:\n    name: Scorecard analysis\n    runs-on: ubuntu-latest\n    permissions:\n      # Needed to upload the results to code-scanning dashboard.\n      security-events: write\n      # Needed to publish results and get a badge (see publish_results below).\n      id-token: write\n      # Uncomment the permissions below if installing in a private repository.\n      # contents: read\n      # actions: read\n\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1\n        with:\n          egress-policy: audit\n\n      - name: \"Checkout code\"\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          persist-credentials: false\n\n      - name: \"Run analysis\"\n        uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0\n        with:\n          results_file: results.sarif\n          results_format: sarif\n          # (Optional) \"write\" PAT token. Uncomment the `repo_token` line below if:\n          # - you want to enable the Branch-Protection check on a *public* repository, or\n          # - you are installing Scorecard on a *private* repository\n          # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.\n          # repo_token: ${{ secrets.SCORECARD_TOKEN }}\n\n          # Public repositories:\n          #   - Publish results to OpenSSF REST API for easy access by consumers\n          #   - Allows the repository to include the Scorecard badge.\n          #   - See https://github.com/ossf/scorecard-action#publishing-results.\n          # For private repositories:\n          #   - `publish_results` will always be set to `false`, regardless\n          #     of the value entered here.\n          publish_results: true\n\n      # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF\n      # format to the repository Actions tab.\n      - name: \"Upload artifact\"\n        uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4\n        with:\n          name: SARIF file\n          path: results.sarif\n          retention-days: 5\n\n      # Upload the results to GitHub's code scanning dashboard.\n      - name: \"Upload to code-scanning\"\n        uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5\n        with:\n          sarif_file: results.sarif\n"
  },
  {
    "path": ".gitignore",
    "content": "# Distribution / packaging\ndist/\neggs/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\npoetry.lock\n\n# Unit test / coverage reports\n.coverage\n.coverage.*\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# IDEs/Build Tools (consider including if you use them)\n.idea/\n.vscode/\nbuild/\n\n# Virtual Environments\n.venv/\nenv/\nvenv\n\n# Logs\napp.log\n\n# Local Config\nconfig.ini\n*.ini"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n- repo: https://github.com/gitleaks/gitleaks\n  rev: v8.16.3\n  hooks:\n  - id: gitleaks\n- repo: https://github.com/pre-commit/pre-commit-hooks\n  rev: v4.4.0\n  hooks:\n  - id: end-of-file-fixer\n  - id: trailing-whitespace\n- repo: https://github.com/pylint-dev/pylint\n  rev: v2.17.2\n  hooks:\n  - id: pylint\n- repo: https://github.com/pycqa/flake8\n  rev: ^4.0.1\n  hooks:\n  - id: flake8\n- repo: https://github.com/psf/black\n  rev: ^22.3.2\n  hooks:\n  - id: black\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n- Demonstrating empathy and kindness toward other people\n- Being respectful of differing opinions, viewpoints, and experiences\n- Giving and gracefully accepting constructive feedback\n- Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n- Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n- The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n- Trolling, insulting or derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n- Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\n<ranjan@duck.com>.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior, harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),\nversion 2.0, available [here](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html).\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\nFor 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).\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## Welcome to Amazon Product Details Scraper contributing guide\n\nThank you for investing your time in contributing to our project! We welcome contributions to this project! Here's how you can get involved:\n\n**1. Issues:**\n\n- **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.\n- **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.\n\n**2. Pull Requests:**\n\n- **Fork the repository:** Create a fork of the repository on GitHub.\n- **Clone your fork:** Clone your forked repository to your local machine.\n- **Create a new branch:** Create a new branch for your changes (e.g., `feature/your_feature_name`).\n- **Implement your changes:** Make your modifications to the code.\n- **Write clear commit messages:** Use descriptive commit messages that explain your changes.\n- **Test your changes:** Ensure your changes don't introduce regressions. Consider adding unit tests if applicable.\n- **Push to your branch:** Push your changes to your branch on your forked repository.\n- **Create a pull request:** Create a pull request from your branch to the main branch of the upstream repository.\n- **Address feedback:** We will review your pull request and provide feedback. Be prepared to address any comments or suggestions.\n\n**3. Coding Style:**\n\n- Follow [PEP 8 style guide](https://peps.python.org/pep-0008/) for Python code.\n- Use consistent formatting and indentation.\n\n**4. License:**\n\n- This project is licensed under the MIT License. Please ensure your contributions are compatible with the license.\n\n**5. Code Documentation:**\n\n- Add comments to document your code, especially complex sections.\n- Consider using docstrings to explain functions and classes.\n\n**Thank you for your contributions!**\n\n**Additional Notes:**\n\n- You are not obligated to include all the sections mentioned above. Adapt them to your project's specific needs.\n- Feel free to add a section about setting up development environment if your project has dependencies.\n- Consider including a code of conduct to promote a positive and inclusive community.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Ranjan Mohanty\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# VFS Appointment Bot\n\n[![GitHub License](https://img.shields.io/github/license/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/blob/main/LICENSE)\n[![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)\n[![PyPI - Version](https://img.shields.io/pypi/v/vfs-appointment-bot?logo=pypi)](https://pypi.org/project/vfs-appointment-bot)\n[![Downloads](https://static.pepy.tech/badge/vfs-appointment-bot)](https://pepy.tech/project/vfs-appointment-bot)\n[![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)\n[![GitHub forks](https://img.shields.io/github/forks/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/forks)\n[![GitHub Repo stars](https://img.shields.io/github/stars/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/stargazers)\n\n[![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)\n[![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)\n[![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)\n[![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)\n![Libraries.io dependency status for GitHub repo](https://img.shields.io/librariesio/github/ranjan-mohanty/vfs-appointment-bot)\n[![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)\n\nThis Python script(**vfs-appointment-bot**) automates checking for appointments at VFS Global portal in a specified country.\n\n## Installation\n\nThe `vfs-appointment-bot` script can be installed using two methods:\n\n### 1. Using pip\n\nIt is the preferred method for installing `vfs-appointment-bot`. Here's how to do it:\n\n1. **Create a virtual environment (Recommended):**\n\n   ```bash\n   python3 -m venv venv\n   ```\n\n   This creates a virtual environment named `venv` to isolate project dependencies from your system-wide Python installation (**recommended**).\n\n2. **Activate the virtual environment:**\n\n   **Linux/macOS:**\n\n   ```bash\n   source venv/bin/activate\n   ```\n\n   **Windows:**\n\n   ```bash\n   venv\\Scripts\\activate\n   ```\n\n3. **Install using pip:**\n\n   ```bash\n   pip install vfs-appointment-bot\n   ```\n\n   This will download and install the `vfs-appointment-bot` package and its dependencies into your Python environment.\n\n### 2. Manual Installation\n\nFor an alternative installation method, clone the source code from the project repository and install it manually.\n\n1. **Clone the repository:**\n\n   ```bash\n   git clone https://github.com/ranjan-mohanty/vfs-appointment-bot\n   ```\n\n2. **Navigate to the project directory:**\n\n   ```bash\n   cd vfs-appointment-bot\n   ```\n\n3. **Create a virtual environment (Recommended):**\n\n   ```bash\n   python3 -m venv venv\n   ```\n\n   This creates a virtual environment named `venv` to isolate project dependencies from your system-wide Python installation (**recommended**).\n\n4. **Activate the virtual environment:**\n\n   **Linux/macOS:**\n\n   ```bash\n   source venv/bin/activate\n   ```\n\n   **Windows:**\n\n   ```bash\n   venv\\Scripts\\activate\n   ```\n\n5. **Install dependencies:**\n\n   ```bash\n   pip install poetry\n   poetry install\n   ```\n\n6. **Install playwright dependencies:**\n\n   ```bash\n   playwright install\n   ```\n\n## Configuration\n\n1. Download the [`config/config.ini`](https://raw.githubusercontent.com/ranjan-mohanty/vfs-appointment-bot/main/config/config.ini) template.\n\n   ```bash\n   curl -L https://raw.githubusercontent.com/ranjan-mohanty/vfs-appointment-bot/main/config/config.ini -o config.ini\n   ```\n\n2. Update the vfs credentials and notification channel preferences. See the [Notification Channels](#notification-channels) section for details on configuring email, Twilio, and Telegram notifications.\n3. Export the path of the config file to the environment variable `VFS_BOT_CONFIG_PATH`\n\n   ```bash\n   export VFS_BOT_CONFIG_PATH=<your-config-path>/config.ini\n   ```\n\n**If you installed the script by cloning the repository (manual installation)**, you can directly edit the values in `config/config.ini`.\n\n## Usage\n\n1. **Command-Line Argument:**\n\n   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.\n\n2. **Running the Script:**\n\n   There are two ways to provide required appointment details:\n\n   - **Responding to User Prompts (recommended):**\n\n     ```bash\n     vfs-appointment-bot -sc IN -dc DE\n     ```\n\n     The script will prompt you to enter the required apponitment parameters for the specified country.\n\n   - **Using `-ap` or `--appointment-params`:**\n\n     Specify appointment details in a comma-separated (**not space-separated**) key-value format:\n\n     ```bash\n     vfs-appointment-bot -sc IN -dc DE -ap visa_center=X,visa_category=Y,visa_sub_category=Z\n     ```\n\n   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).\n\n## Notification Channels\n\nIt currently supports three notification channels to keep you informed about appointment availability:\n\n- **Email:** Sends notifications via a Gmail account.\n- **Twilio (SMS & Voice Call):** Enables alerts through text messages and phone calls using Twilio's services.\n- **Telegram:** Sends notifications directly to your Telegram account through a bot.\n\n**Configuring Notifications:**\n\n**Email:**\n\n1. **Email Account:** You'll need a **Gmail account** for sending notifications.\n2. **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).\n3. **Configuration File:** Update your application's configuration file (`config.ini`) with the following details:\n\n   - **`email` (Required):** Your Gmail address.\n   - **`password` (Required):** Your generated Gmail app password.\n\n**Twilio:**\n\n1. **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.\n2. **Retrieve Credentials:** Locate your account SID, auth token, and phone numbers within your Twilio account dashboard.\n3. **Configuration File:** Update your application's configuration file (`config.ini`) with:\n\n   - `auth_token` (Required): Your Twilio auth token\n   - `account_sid` (Required): Your Twilio account SID\n   - `sms_enabled` (Optional): Enables SMS notifications (default: True)\n   - `call_enabled` (Optional): Enables voice call notifications (default: False)\n   - `url` (Optional): Twilio API URL (Only needed if call is enabled)\n   - `to_num` (Required): Recipient phone number for notifications\n   - `from_num` (Required): Twilio phone number you'll use for sending messages\n\n**Telegram:**\n\n1. **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.\n2. **Configuration File:** Update your application's configuration file (`config.ini`) with:\n\n   - **`bot_token` (Required):** Your Telegram bot token obtained from BotFather.\n   - **`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.\n\n## Supported Countries and Appointment Parameters\n\nThe following table lists currently supported countries and their corresponding appointment parameters:\n\n| Country                    | Appointment Parameters                                      |\n| -------------------------- | ----------------------------------------------------------- |\n| India(IN) - Germany(DE)    | visa_category, visa_sub_category, visa_center               |\n| Iraq(IQ) - Germany(DE)     | visa_category, visa_sub_category, visa_center               |\n| Morocco(MA) - Italy(IT)    | visa_category, visa_sub_category, visa_center, payment_mode |\n| Azerbaijan(AZ) - Italy(IT) | visa_category, visa_sub_category, visa_center               |\n\n**Notes:**\n\n- Appointment parameters might vary depending on the specific country and visa type. Always consult VFS Global's website for the latest information.\n\n## Known Issues\n\n**1. Login Failures After Frequent Requests:**  \nIf 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.\n\n- **Workaround:**\n  - **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.\n  - **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.\n\n**2. Occasional Captcha Verification:**  \nThe VFS website requires a CAPTCHA verification step during login. Currently, the bot does not have a built-in CAPTCHA solver.\n\n- **Workaround:**\n  - **Wait and Retry:** Sometimes, CAPTCHAs appear due to temporary website issues. Wait for a while and try again later.\n  - **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.\n\n**Note:** We are constantly working to improve the bot's functionality. Future updates might include integrated CAPTCHA solving capabilities.\n\n## Extending Country Support\n\nThis 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.\n\n## Contributing\n\nWe welcome contributions from the community to improve this project! Here's how you can get involved:\n\n- **Report issues:** If you encounter any bugs or problems with the script, please create an issue on the project's repository.\n- **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.\n- **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.\n\n## Star History\n\n[![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)\n\n## Disclaimer\n\nThis 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.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "## Security Policy for VFS Appointment Bot\n\nThis document outlines the security policy for the VFS Appointment Bot project.\n\n**1. Reporting Vulnerabilities:**\n\nWe appreciate your help in keeping this project secure. If you discover a security vulnerability, please report it responsibly by following these steps:\n\n**1.1 Public Reporting:**\n\n- If the vulnerability can be disclosed publicly without compromising security, you can create a public issue report on the project's GitHub repository.\n\n**1.2 Private Reporting:**\n\n- **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)\n\n- **Detailed Description:** Provide a detailed description of the vulnerability, including steps to reproduce it and potential impact.\n- **Confidentiality:** Keep the vulnerability confidential until a fix is released to prevent exploitation.\n\nWe will acknowledge your report and work on a fix with the following goals:\n\n- **Timely Response:** We will address reported vulnerabilities as quickly as possible.\n- **Transparency:** We will keep you informed of the progress towards a fix and its estimated release date.\n- **Fix Release:** We will release a fix for the vulnerability in a timely manner.\n\n**2. Secure Coding Practices:**\n\nThe script development follows best practices for secure coding to minimize vulnerabilities. These practices include:\n\n- **Input Validation:** User input is sanitized to prevent injection attacks (e.g., SQL injection, XSS).\n- **Dependency Management:** Dependencies are kept up-to-date to address known vulnerabilities in external libraries.\n- **Secret Handling:** Sensitive information (if any) is not stored in plain text.\n\n**3. Supported Versions:**\n\nWe 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.\n\n**4. Disclaimer:**\n\nWhile 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.\n\n**5. Responsible Use:**\n\nThis 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.\n\n**6. Reporting Abuses:**\n\nIf you suspect any misuse of this script for malicious purposes, please contact the project maintainer immediately.\n\nWe appreciate your cooperation in using this script responsibly!\n"
  },
  {
    "path": "pyproject.toml",
    "content": "# Project metadata\n[tool.poetry]\nname = \"vfs-appointment-bot\"\nversion = \"1.1.1\"\ndescription = \"VFS Appointment Bot - This script automates checking for appointments at VFS Global offices in a specified country.\"\nauthors = [\"Ranjan Mohanty <ranjan@duck.com>\"]\nlicense = \"MIT\"\nreadme = \"README.md\"\nkeywords = [\"vfs\", \"vfs-bot\", \"vfs-appointment-bot\", \"visa-appointment-bot\"]\n\n# URLs\n[tool.poetry.urls]\nrepository = \"https://github.com/ranjan-mohanty/vfs-appointment-bot/blob/main/README.md\"\nhomepage = \"https://github.com/ranjan-mohanty/vfs-appointment-bot/blob/main\"\n\n# Build System\n[build-system]\nrequires = [\"poetry-core\"]\nbuild-backend = \"poetry.core.masonry.api\"\n\n# Dependencies\n[tool.poetry.dependencies]\npython = \"^3.9\"\nplaywright = \"^1.43.0\"\nplaywright-stealth = \"^1.0.6\"\ntwilio = \"^9.0.4\"\ntqdm = \"^4.66.2\"\n\n[tool.poetry.dev-dependencies]\nflake8 = \"^7.0.0\"\nblack = \"^24.4.2\"\n\n#Entry points\n[tool.poetry.scripts]\nvfs-appointment-bot = \"vfs_appointment_bot.main:main\"\n"
  },
  {
    "path": "vfs_appointment_bot/main.py",
    "content": "import argparse\nimport logging\nimport sys\nfrom typing import Dict\n\nfrom vfs_appointment_bot.utils.config_reader import get_config_value, initialize_config\nfrom vfs_appointment_bot.utils.timer import countdown\nfrom vfs_appointment_bot.vfs_bot.vfs_bot import LoginError\nfrom vfs_appointment_bot.vfs_bot.vfs_bot_factory import (\n    UnsupportedCountryError,\n    get_vfs_bot,\n)\n\n\nclass KeyValueAction(argparse.Action):\n    \"\"\"Custom action class for parsing appointment parameters.\n\n    This class handles parsing comma-separated key-value pairs provided through\n    the `--appointment-params` argument. It ensures the format is valid (key=value)\n    and stores the parsed parameters as a dictionary.\n    \"\"\"\n\n    def __call__(self, parser, namespace, values, option_string=None):\n        try:\n            appointment_params: Dict[str, str] = {\n                key.strip(): value.strip()\n                for key, value in (item.split(\"=\") for item in values.split(\",\"))\n            }\n            setattr(namespace, \"appointment_params\", appointment_params)\n        except ValueError:\n            parser.error(\n                f\"Invalid value format for {option_string}, use key=value pairs\"\n            )\n\n\ndef main() -> None:\n    \"\"\"\n    Entry point for the VFS Appointment Bot.\n\n    This function sets up logging, parses command-line arguments, and runs the VFS appointment\n    checking process in a continuous loop. It catches exceptions for unsupported countries and\n    unexpected errors, logging them appropriately.\n\n    Raises:\n        UnsupportedCountryError: If the provided country code is not supported by the bot.\n        Exception: For any other unexpected errors encountered during execution.\n    \"\"\"\n    initialize_logger()\n    initialize_config()\n\n    parser = argparse.ArgumentParser(\n        description=\"VFS Appointment Bot: Checks for appointments at VFS Global\"\n    )\n    required_args = parser.add_argument_group(\"required arguments\")\n    required_args.add_argument(\n        \"-sc\",\n        \"--source-country-code\",\n        type=str,\n        help=\"The ISO 3166-1 alpha-2 source country code (refer to README)\",\n        metavar=\"<country_code>\",\n        required=True,\n    )\n\n    required_args.add_argument(\n        \"-dc\",\n        \"--destination-country-code\",\n        type=str,\n        help=\"The ISO 3166-1 alpha-2 destination country code (refer to README)\",\n        metavar=\"<country_code>\",\n        required=True,\n    )\n\n    parser.add_argument(\n        \"-ap\",\n        \"--appointment-params\",\n        type=str,\n        default=None,\n        help=\"Comma-separated key-value pairs for additional appointment details (refer to VFS website)\",\n        action=KeyValueAction,\n        metavar=\"<key1=value1,key2=value2,...>\",\n    )\n\n    args = parser.parse_args()\n    source_country_code = args.source_country_code\n    destination_country_code = args.destination_country_code\n    try:\n        while True:\n            vfs_bot = get_vfs_bot(source_country_code, destination_country_code)\n            appointment_found = vfs_bot.run(args)\n            if appointment_found:\n                break\n            countdown(\n                int(get_config_value(\"default\", \"interval\")),\n                \"Next appointment check in\",\n            )\n\n    except (UnsupportedCountryError, LoginError) as e:\n        logging.error(e)\n    except Exception as e:\n        logging.exception(e)\n\n\ndef initialize_logger():\n    file_handler = logging.FileHandler(\"app.log\", mode=\"a\")\n    file_handler.setFormatter(\n        logging.Formatter(\n            \"[%(asctime)s] %(levelname)s [%(filename)s:%(lineno)d] %(message)s\"\n        )\n    )\n\n    stream_handler = logging.StreamHandler(sys.stdout)\n    stream_handler.setFormatter(logging.Formatter(\"[%(asctime)s] %(message)s\"))\n    logging.basicConfig(\n        level=logging.INFO,\n        format=\"[%(asctime)s] %(levelname)s [%(filename)s:%(lineno)d] %(message)s\",\n        handlers=[\n            file_handler,\n            stream_handler,\n        ],\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "vfs_appointment_bot/notification/email_client.py",
    "content": "import smtplib\nimport logging\n\nfrom vfs_appointment_bot.notification.notification_client import NotificationClient\n\n\nclass EmailClient(NotificationClient):\n    def __init__(self):\n        \"\"\"\n        Initializes the email client with configuration data.\n\n        This constructor retrieves configuration settings from the designated\n        section (e.g., `\"email\"`) of the application configuration and\n        validates them using the base class validation logic.\n        \"\"\"\n        required_keys = [\"email\", \"password\"]\n        super().__init__(\"email\", required_keys)\n\n    def send_notification(self, message: str) -> None:\n        \"\"\"\n        Sends a notification message through the email channel.\n\n        This method sends an email notification using the provided message content.\n        It connects securely to the configured SMTP server (e.g., Gmail's SMTP),\n        authenticates with the provided credentials, and constructs a well-formatted\n        email before sending it.\n\n        Args:\n            message (str): The message content to be included in the email.\n        \"\"\"\n        email: str = self.config.get(\"email\")\n        password: str = self.config.get(\"password\")\n        email_text = self.__construct_email_text(email, message)\n\n        smtp_server = smtplib.SMTP_SSL(\"smtp.gmail.com\", 465)\n        smtp_server.ehlo()\n        smtp_server.login(email, password)\n        smtp_server.sendmail(email, email, email_text)\n        smtp_server.close()\n        logging.info(\"Email sent successfully!\")\n\n    def __construct_email_text(self, email: str, message: str) -> str:\n        \"\"\"\n        Constructs a formatted email text with sender, receiver, subject,\n        and message body.\n\n        Args:\n            message (str): The message content to be included in the email body.\n\n        Returns:\n            str: The formatted email text ready for sending.\n        \"\"\"\n        return f\"From: {email}\\nTo: {email}\\nSubject: VFS Appointment Bot Notification\\n\\n{message}\"\n"
  },
  {
    "path": "vfs_appointment_bot/notification/notification_client.py",
    "content": "from abc import ABC, abstractmethod\nfrom typing import List\n\nfrom vfs_appointment_bot.utils.config_reader import get_config_section\n\n\nclass NotificationClient(ABC):\n    \"\"\"Abstract base class for notification clients.\n\n    This class defines the common interface for notification clients used\n    throughout the application. Subclasses must implement the `send_notification`\n    method to provide specific notification sending functionality for their\n    respective channels.\n    \"\"\"\n\n    def __init__(self, config_section: str, required_config_keys: List[str]):\n        \"\"\"\n        Initializes the client with configuration data.\n\n        Args:\n            config_section (str): The name of the configuration section\n                containing client-specific settings.\n            required_config_keys (list[str]): A list of required keys that must be\n                present in the configuration section.\n        \"\"\"\n        self.required_keys = required_config_keys\n        self.config = get_config_section(config_section)\n        self._validate_config(required_config_keys)\n\n    @abstractmethod\n    def send_notification(self, message: str) -> None:\n        \"\"\"\n        Sends a notification message to the recipient.\n\n        This method is abstract and must be implemented by subclasses to\n        provide the specific logic for sending notifications through their\n        respective channels.\n\n        Args:\n            message (str): The message content to be sent.\n        \"\"\"\n\n    def _validate_config(self, required_config_keys: list[str]):\n        \"\"\"\n        Validates the configuration of the notification client.\n\n        This method checks if all required configuration keys are present\n        and have non-null values. If any validation errors are found,\n        appropriate exceptions are raised.\n\n        Args:\n            required_config_keys (list[str]): A list of required keys that must be\n                present in the configuration section.\n        \"\"\"\n        missing_keys = required_config_keys - self.config.keys()\n        if missing_keys:\n            raise NotificationClientConfigValidationError(\n                f\"Missing required configuration keys: {', '.join(missing_keys)}\"\n            )\n\n        for key in self.required_keys:\n            if self.config.get(key) is None:\n                raise NotificationClientConfigValidationError(\n                    f\"Value for key '{key}' cannot be null.\"\n                )\n\n\nclass NotificationClientConfigValidationError(Exception):\n    \"\"\"Exception raised when notification client configuration validation fails.\"\"\"\n\n\nclass NotificationClientError(Exception):\n    \"\"\"Exception raised when an error occurs during notification sending.\"\"\"\n"
  },
  {
    "path": "vfs_appointment_bot/notification/notification_client_factory.py",
    "content": "from vfs_appointment_bot.notification.notification_client import NotificationClient\n\n\nclass UnsupportedNotificationChannelError(Exception):\n    \"\"\"Raised when an unsupported notification channel is provided.\"\"\"\n\n\ndef get_notification_client(channel: str) -> NotificationClient:\n    \"\"\"Retrieves the appropriate notification client for a given channel.\n\n    This function creates an instance of a notification client class based on the\n    provided channel string. Currently supported channels include \"telegram\" and\n    \"slack\". If an unsupported channel is provided, a `ValueError` exception is\n    raised.\n\n    Args:\n        channel (str): The notification channel name.\n\n    Returns:\n        NotificationClient: An instance of the `NotificationClient` sub class specific to the provided channel.\n\n    Raises:\n        UnsupportedNotificationChannelError: If the provided notification channel is not supported.\n    \"\"\"\n\n    if channel == \"telegram\":\n        from .telegram_client import TelegramClient\n\n        return TelegramClient()\n    elif channel == \"slack\":\n        from .twilio_client import TwilioClient\n\n        return TwilioClient()\n    elif channel == \"email\":\n        from .email_client import EmailClient\n\n        return EmailClient()\n    else:\n        raise UnsupportedNotificationChannelError(\n            f\"Notification channel '{channel}' is not supported\"\n        )\n"
  },
  {
    "path": "vfs_appointment_bot/notification/telegram_client.py",
    "content": "import logging\n\nimport requests\n\nfrom vfs_appointment_bot.notification.notification_client import NotificationClient\n\n\nclass TelegramClient(NotificationClient):\n    \"\"\"Concrete implementation of NotificationClient for the Telegram channel.\n\n    This class provides functionality for sending notifications through the Telegram\n    messaging platform. It inherits from the abstract `NotificationClient` class\n    and implements the required `send_notification` method for Telegram-specific\n    notification sending logic.\n    \"\"\"\n\n    def __init__(self):\n        \"\"\"\n        Initializes the Telegram client with configuration data.\n\n        This constructor retrieves configuration settings from the \"telegram\"\n        section of the application configuration and validates them using the\n        base class validation logic.\n        \"\"\"\n        required_keys = [\"bot_token\", \"chat_id\", \"parse_mode\"]\n        super().__init__(\"telegram\", required_keys)\n\n    def send_notification(self, message: str) -> None:\n        \"\"\"\n        Sends a notification message through the Telegram channel.\n\n        This method constructs a Telegram API request URL using the retrieved\n        configuration settings (bot token, chat ID, and parse mode) and sends a\n        GET request to the Telegram API with the message content. The response\n        from the Telegram API is logged for debugging purposes.\n\n        Args:\n            message (str): The message content to be sent as a Telegram notification.\n        \"\"\"\n        bot_token: str = self.config.get(\"bot_token\")\n        chat_id: str = self.config.get(\"chat_id\")\n        parse_mode: str = self.config.get(\"parse_mode\")\n\n        url = (\n            f\"https://api.telegram.org/bot{bot_token}/sendMessage?\"\n            + f\"chat_id={chat_id}&parse_mode={parse_mode}&text={message}\"\n        )\n        requests.get(url, timeout=3000).json()\n        logging.info(\"Telegram message sent successfully!\")\n"
  },
  {
    "path": "vfs_appointment_bot/notification/twilio_client.py",
    "content": "import logging\nfrom typing import Optional\n\nfrom twilio.rest import Client\n\nfrom vfs_appointment_bot.notification.notification_client import NotificationClient\n\n\nclass TwilioClient(NotificationClient):\n    \"\"\"Concrete implementation of NotificationClient for the Twilio channel.\n\n    This class provides functionality for sending notifications through the Twilio\n    communication platform. It inherits from the abstract `NotificationClient` class\n    and implements the required `send_notification` method for Twilio-specific\n    notification sending logic, including SMS messages and calls (if enabled).\n    \"\"\"\n\n    def __init__(self):\n        \"\"\"\n        Initializes the Twilio client with configuration data.\n\n        This constructor retrieves configuration settings from the \"twilio\"\n        section of the application configuration and validates them using the\n        base class validation logic.\n        \"\"\"\n        required_config_keys = [\n            \"to_num\",\n            \"from_num\",\n            \"account_sid\",\n            \"auth_token\",\n            \"url\",\n            \"call_enabled\",\n        ]\n        super().__init__(\"twilio\", required_config_keys)\n\n    def send_notification(self, message: str) -> None:\n        \"\"\"\n        Sends a notification message through the Twilio channel.\n\n        This method sends an SMS message using the provided message content.\n        Optionally, if the \"call_enabled\" flag is set to True in the\n        configuration, it also initiates a call to the specified phone number\n        using a pre-recorded URL (provided by the \"url\" configuration option).\n\n        Args:\n            message (str): The message content to be sent as a Twilio SMS.\n        \"\"\"\n        url: Optional[str] = self.config.get(\"url\")\n        auth_token: str = self.config.get(\"auth_token\")\n        account_sid: str = self.config.get(\"account_sid\")\n        to_num: str = self.config.get(\"to_num\")\n        from_num: str = self.config.get(\"from_num\")\n        call_enabled: bool = self.config.get(\"call_enabled\", False)\n\n        self.__send_message(message, auth_token, account_sid, to_num, from_num)\n\n        if call_enabled:\n            self.__call(url, auth_token, account_sid, to_num, from_num)\n\n    def __send_message(\n        self,\n        message: str,\n        auth_token: str,\n        account_sid: str,\n        to_num: str,\n        from_num: str,\n    ) -> None:\n        \"\"\"\n        Sends an SMS message using the Twilio API.\n\n        This private helper method creates a Twilio client instance and uses it\n        to send an SMS message with the provided content to the specified recipient\n        phone number.\n\n        Args:\n            message (str): The message content to be sent.\n            auth_token (str): The Twilio account authentication token.\n            account_sid (str): The Twilio account SID.\n            to_num (str): The recipient phone number.\n            from_num (str): The Twilio phone number used to send the message.\n        \"\"\"\n        client = Client(account_sid, auth_token)\n        client.messages.create(to=to_num, from_=from_num, body=message)\n        logging.info(\"Message sent successfully!\")\n\n    def __call(\n        self,\n        url: Optional[str],\n        auth_token: str,\n        account_sid: str,\n        to_num: str,\n        from_num: str,\n    ) -> None:\n        \"\"\"\n        Initiates a call using the Twilio API (if URL is provided).\n\n        This private helper method creates a Twilio client instance and uses it\n        to initiate a call to the specified recipient phone number, using a\n        pre-recorded URL for the call content (if provided in the configuration).\n\n        Args:\n            url (Optional[str]): The URL for the pre-recorded call content.\n            auth_token (str): The Twilio account authentication token.\n            account_sid (str): The Twilio account SID.\n            to_num (str): The recipient phone number.\n            from_num (str): The Twilio phone number used to initiate the call.\n        \"\"\"\n        if url:\n            client = Client(account_sid, auth_token)\n            client.calls.create(from_=from_num, to=to_num, url=url)\n            logging.info(\"Call request sent successfully!\")\n        else:\n            logging.warning(\"No URL provided for call request!\")\n"
  },
  {
    "path": "vfs_appointment_bot/utils/config_reader.py",
    "content": "import os\nfrom configparser import ConfigParser\nfrom typing import Dict\n\n_config: ConfigParser = None\n\n\ndef initialize_config(config_dir=\"config\"):\n    \"\"\"\n    Reads all INI configuration files in a directory and caches the result.\n    Also reads user config from `VFS_BOT_CONFIG_PATH` env var (if set)\n\n    Args:\n        config_dir: The directory containing configuration files (default: \"config\").\n    \"\"\"\n    global _config\n    if not _config:\n        _config = ConfigParser()\n        for entry in os.scandir(config_dir):\n            if entry.is_file() and entry.name.endswith(\".ini\"):\n                config_file_path = os.path.join(config_dir, entry.name)\n                _config.read(config_file_path)\n\n    # Read user defined config file\n    user_config_path = os.environ.get(\"VFS_BOT_CONFIG_PATH\")\n    if user_config_path:\n        _config.read(user_config_path)\n\n\ndef get_config_section(section: str, default: Dict = None) -> Dict:\n    \"\"\"\n    Get a configuration section as a dictionary.\n\n    Args:\n        section: The name of the section to retrieve.\n        default: A dictionary containing default values for the section (optional).\n\n    Returns:\n        A dictionary containing the configuration for the specified section,\n        or the provided default dictionary if the section is not found.\n    \"\"\"\n    if _config.has_section(section):\n        return dict(_config[section])\n    else:\n        return default or {}\n\n\ndef get_config_value(section: str, key: str, default: str = None) -> str:\n    \"\"\"\n    Get a specific configuration value.\n\n    Args:\n        section: The name of the section containing the value.\n        key: The name of the key to retrieve.\n        default: The default value to return if the section or key is not found (optional).\n\n    Returns:\n        The value associated with the given key within the specified section,\n        or the provided default value if the section or key does not exist.\n    \"\"\"\n    if _config.has_section(section) and _config.has_option(section, key):\n        return _config[section][key]\n    else:\n        return default\n"
  },
  {
    "path": "vfs_appointment_bot/utils/date_utils.py",
    "content": "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})\"\n    match = re.search(pattern, text)\n    if match:\n        return match.group()\n    else:\n        return None\n"
  },
  {
    "path": "vfs_appointment_bot/utils/timer.py",
    "content": "import time\n\nfrom tqdm import tqdm\n\n\ndef countdown(t: int, message=\"Countdown\", unit=\"seconds\"):\n    \"\"\"\n    Implements a countdown timer\n\n    Args:\n        t (int): The initial countdown time in the specified unit (e.g., seconds, minutes).\n        message (str, optional): The message to display during the countdown.\n                                Defaults to \"Countdown\".\n        unit (str, optional): The unit of time for the countdown. Defaults to \"seconds\".\n    \"\"\"\n    with tqdm(\n        total=t, desc=message, unit=unit, bar_format=\"{desc}: {remaining}\"\n    ) as pbar:\n        for _ in range(t):\n            time.sleep(1)\n            pbar.update(1)\n"
  },
  {
    "path": "vfs_appointment_bot/vfs_bot/vfs_bot.py",
    "content": "import argparse\nimport logging\nfrom abc import ABC, abstractmethod\nfrom typing import Dict, List\n\nimport playwright\nfrom playwright.sync_api import sync_playwright\nfrom playwright_stealth import stealth_sync\n\nfrom vfs_appointment_bot.utils.config_reader import get_config_value\nfrom vfs_appointment_bot.notification.notification_client_factory import (\n    get_notification_client,\n)\n\n\nclass LoginError(Exception):\n    \"\"\"Exception raised when login fails.\"\"\"\n\n\nclass VfsBot(ABC):\n    \"\"\"\n    Abstract base class for VfsBot\n\n    Provides common functionalities like login, pre-login steps, appointment checking, and notification.\n    Subclasses are responsible for implementing country-specific login and appointment checking logic.\n    \"\"\"\n\n    def __init__(self):\n        \"\"\"\n        Initializes a VfsBot instance for a specific country.\n\n        \"\"\"\n        self.source_country_code = None\n        self.destination_country_code = None\n        self.appointment_param_keys: List[str] = []\n\n    def run(self, args: argparse.Namespace = None) -> bool:\n        \"\"\"\n        Starts the VFS bot for appointment checking and notification.\n\n        This method reads configuration values, performs login, checks for\n        appointments based on provided arguments, and sends notifications if\n        appointments are found.\n\n        Args:\n            args (argparse.Namespace, optional): Namespace object containing parsed\n                command-line arguments. Defaults to None.\n\n        Returns:\n            bool: True if appointments were found, False otherwise.\n        \"\"\"\n\n        logging.info(\n            f\"Starting VFS Bot for {self.source_country_code.upper()}-{self.destination_country_code.upper()}\"\n        )\n\n        # Configuration values\n        try:\n            browser_type = get_config_value(\"browser\", \"type\", \"firefox\")\n            headless_mode = get_config_value(\"browser\", \"headless\", \"True\")\n            url_key = self.source_country_code + \"-\" + self.destination_country_code\n            vfs_url = get_config_value(\"vfs-url\", url_key)\n        except KeyError as e:\n            logging.error(f\"Missing configuration value: {e}\")\n            return\n\n        email_id = get_config_value(\"vfs-credential\", \"email\")\n        password = get_config_value(\"vfs-credential\", \"password\")\n\n        appointment_params = self.get_appointment_params(args)\n\n        # Launch browser and perform actions\n        with sync_playwright() as p:\n            browser = getattr(p, browser_type).launch(\n                headless=headless_mode in (\"True\", \"true\")\n            )\n            page = browser.new_page()\n            stealth_sync(page)\n\n            page.goto(vfs_url)\n            self.pre_login_steps(page)\n\n            try:\n                self.login(page, email_id, password)\n                logging.info(\"Logged in successfully\")\n            except Exception:\n                browser.close()\n                raise LoginError(\n                    \"\\033[1;31mLogin failed. \"\n                    + \"Please verify your username and password by logging in to the browser and try again.\\033[0m\"\n                )\n\n            logging.info(f\"Checking appointments for {appointment_params}\")\n            appointment_found = False\n            try:\n                dates = self.check_for_appontment(page, appointment_params)\n                if dates:\n                    # Log successful appointment finding\n                    logging.info(\n                        f\"\\033[1;32mFound appointments on: {', '.join(dates)} \\033[0m\"\n                    )\n                    self.notify_appointment(appointment_params, dates)\n                    appointment_found = True\n                else:\n                    # Log no appointments found\n                    logging.info(\n                        \"\\033[1;33mNo appointments found for the specified criteria.\\033[0m\"\n                    )\n            except Exception as e:\n                logging.error(f\"Appointment check failed: {e}\")\n            browser.close()\n            return appointment_found\n\n    def get_appointment_params(self, args: argparse.Namespace) -> Dict[str, str]:\n        \"\"\"\n        Collects appointment parameters from command-line arguments or user input.\n\n        This method iterates through pre-defined `appointment_param_keys` (replace\n        with relevant keys) and retrieves values either from provided arguments\n        or prompts the user for input if values are missing.\n\n        Args:\n            args (argparse.Namespace): Namespace object containing parsed command-line arguments.\n\n        Returns:\n            Dict[str, str]: A dictionary containing appointment parameters.\n        \"\"\"\n        appointment_params = {}\n        for key in self.appointment_param_keys:\n            if (\n                getattr(args, \"appointment_params\") is not None\n                and args.appointment_params[key] is not None\n            ):\n                appointment_params[key] = args.appointment_params[key]\n            else:\n                key_name = key.replace(\"_\", \" \")\n                appointment_params[key] = input(f\"Enter the {key_name}: \")\n        return appointment_params\n\n    def notify_appointment(self, appointment_params: Dict[str, str], dates: List[str]):\n        \"\"\"\n        Sends appointment dates notification to the user.\n\n        This method is responsible for notifying the appointment dates to the user configured channels\n\n        Args:\n            dates (List[str]): A list of appointment dates.\n            appointment_params (Dict[str, str]): A dictionary containing appointment search criteria.\n        \"\"\"\n        message = f\"Found appointment(s) for {', '.join(appointment_params.values())} on {', '.join(dates)}\"\n        channels = get_config_value(\"notification\", \"channels\")\n        if len(channels) == 0:\n            logging.warning(\n                \"No notification channels configured. Skipping notification.\"\n            )\n            return\n\n        for channel in channels.split(\",\"):\n            client = get_notification_client(channel)\n            try:\n                client.send_notification(message)\n            except Exception:\n                logging.error(f\"Failed to send {channel} notification\")\n\n    @abstractmethod\n    def login(\n        self, page: playwright.sync_api.Page, email_id: str, password: str\n    ) -> None:\n        \"\"\"\n        Performs login steps specific to the VFS website for the bot's country.\n\n        This abstract method needs to be implemented by subclasses to handle\n        country-specific login procedures (e.g., filling login form elements, handling\n        CAPTCHAs). It should interact with the Playwright `page` object to achieve\n        login functionality.\n\n        Args:\n            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.\n            email_id (str): The user's email address for VFS login.\n            password (str): The user's password for VFS login.\n\n        Raises:\n            Exception: If login fails due to unexpected errors.\n        \"\"\"\n        raise NotImplementedError(\"Subclasses must implement login logic\")\n\n    @abstractmethod\n    def pre_login_steps(self, page: playwright.sync_api.Page) -> None:\n        \"\"\"\n        Performs any pre-login steps required by the VFS website for the bot's country.\n\n        This abstract method allows subclasses to implement country-specific actions\n        that need to be done before login (e.g., cookie acceptance, language selection).\n        It should interact with the Playwright `page` object to perform these actions.\n\n        Args:\n            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.\n        \"\"\"\n\n    @abstractmethod\n    def check_for_appontment(\n        self, page: playwright.sync_api.Page, appointment_params: Dict[str, str]\n    ) -> List[str]:\n        \"\"\"\n        Checks for appointments based on provided parameters on the VFS website.\n\n        This abstract method needs to be implemented by subclasses to interact with\n        the VFS website and search for appointments based on the given `appointment_params`\n        dictionary. It should use the Playwright `page` object to navigate the website\n        and extract appointment dates.\n\n        Args:\n            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.\n            appointment_params (Dict[str, str]): A dictionary containing appointment search criteria.\n\n        Returns:\n            List[str]: A list of available appointment dates (empty list if none found).\n        \"\"\"\n        raise NotImplementedError(\n            \"Subclasses must implement appointment checking logic\"\n        )\n"
  },
  {
    "path": "vfs_appointment_bot/vfs_bot/vfs_bot_de.py",
    "content": "import logging\nfrom typing import Dict, List, Optional\n\nfrom playwright.sync_api import Page\n\nfrom vfs_appointment_bot.utils.date_utils import extract_date_from_string\nfrom vfs_appointment_bot.vfs_bot.vfs_bot import VfsBot\n\n\nclass VfsBotDe(VfsBot):\n    \"\"\"Concrete implementation of VfsBot for Germany (DE).\n\n    This class inherits from the base `VfsBot` class and implements\n    country-specific logic for interacting with the VFS website for Germany.\n    It overrides the following methods to handle German website specifics:\n\n    - `login`: Fills the login form elements with email and password.\n    - `pre_login_steps`: Rejects all cookie policies if presented.\n    - `check_for_appontment`: Performs appointment search based on provided\n        parameters and extracts available dates from the website.\n    \"\"\"\n\n    def __init__(self, source_country_code: str):\n        \"\"\"\n        Initializes a VfsBotDe instance for Germany.\n\n        This constructor sets the source country code and the destination country\n        code \"de\"(Germany). It also defines appointment parameter keys specific\n        to the destination country's VFS website.\n\n        Args:\n            source_country_code (str): The country code where you're applying from.\n        \"\"\"\n        super().__init__()\n        self.source_country_code = source_country_code\n        self.destination_country_code = \"DE\"\n        self.appointment_param_keys = [\n            \"visa_center\",\n            \"visa_category\",\n            \"visa_sub_category\",\n        ]\n\n    def login(self, page: Page, email_id: str, password: str) -> None:\n        \"\"\"\n        Performs login steps specific to the German VFS website.\n\n        This method fills the email and password input fields on the login form\n        and clicks the \"Sign In\" button. It raises an exception if the login fails\n        (e.g., if the \"Start New Booking\" button is not found after login).\n\n        Args:\n            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.\n            email_id (str): The user's email address for VFS login.\n            password (str): The user's password for VFS login.\n\n        Raises:\n            Exception: If login fails due to unexpected errors or missing \"Start New Booking\" button.\n        \"\"\"\n        email_input = page.locator(\"#mat-input-0\")\n        password_input = page.locator(\"#mat-input-1\")\n\n        email_input.fill(email_id)\n        password_input.fill(password)\n\n        page.get_by_role(\"button\", name=\"Sign In\").click()\n        page.wait_for_selector(\"role=button >> text=Start New Booking\")\n\n    def pre_login_steps(self, page: Page) -> None:\n        \"\"\"\n        Performs pre-login steps specific to the German VFS website.\n\n        This method checks for a \"Reject All\" button for cookie policies and\n        clicks it if found.\n\n        Args:\n            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.\n        \"\"\"\n        policies_reject_button = page.get_by_role(\"button\", name=\"Reject All\")\n        if policies_reject_button is not None:\n            policies_reject_button.click()\n            logging.debug(\"Rejected all cookie policies\")\n\n    def check_for_appontment(\n        self, page: Page, appointment_params: Dict[str, str]\n    ) -> Optional[List[str]]:\n        \"\"\"\n        Checks for appointments on the German VFS website based on provided parameters.\n\n        This method clicks the \"Start New Booking\" button, selects the specified\n        visa center, category, and subcategory based on the `appointment_params`\n        dictionary. It then extracts the available appointment dates from the\n        website and returns them as a list. If no appointments are found, it\n        returns None.\n\n        Args:\n            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.\n            appointment_params (Dict[str, str]): A dictionary containing appointment search criteria.\n\n        Returns:\n            Optional[List[str]]: A list of available appointment dates (empty list if none found),\n                including a timestamp of the check, or None if no appointments found.\n        \"\"\"\n        page.get_by_role(\"button\", name=\"Start New Booking\").click()\n\n        # Select Visa Centre\n\n        visa_centre_dropdown = page.wait_for_selector(\"mat-form-field\")\n        visa_centre_dropdown.click()\n        visa_centre_dropdown_option = page.wait_for_selector(\n            f'mat-option:has-text(\"{appointment_params.get(\"visa_center\")}\")'\n        )\n        visa_centre_dropdown_option.click()\n\n        # Select Visa Category\n        visa_category_dropdown = page.query_selector_all(\"mat-form-field\")[1]\n        visa_category_dropdown.click()\n        visa_category_dropdown_option = page.wait_for_selector(\n            f'mat-option:has-text(\"{appointment_params.get(\"visa_category\")}\")'\n        )\n        visa_category_dropdown_option.click()\n\n        # Select Subcategory\n        visa_subcategory_dropdown = page.query_selector_all(\"mat-form-field\")[2]\n        visa_subcategory_dropdown.click()\n        visa_subcategory_dropdown_option = page.wait_for_selector(\n            f'mat-option:has-text(\"{appointment_params.get(\"visa_sub_category\")}\")'\n        )\n        visa_subcategory_dropdown_option.click()\n\n        try:\n            page.wait_for_selector(\"div.alert\")\n            appointment_date_elements = page.query_selector_all(\"div.alert\")\n            appointment_dates = []\n            for appointment_date_element in appointment_date_elements:\n                appointment_date_text = appointment_date_element.text_content()\n                appointment_date = extract_date_from_string(appointment_date_text)\n                if appointment_date is not None and len(appointment_date) > 0:\n                    appointment_dates.append(appointment_date)\n            return appointment_dates\n        except Exception:\n            return None\n\n        return None\n"
  },
  {
    "path": "vfs_appointment_bot/vfs_bot/vfs_bot_factory.py",
    "content": "from vfs_appointment_bot.vfs_bot.vfs_bot import VfsBot\n\n\nclass UnsupportedCountryError(Exception):\n    \"\"\"Raised when an unsupported country code is provided.\"\"\"\n\n\ndef get_vfs_bot(source_country_code: str, destination_country_code: str) -> VfsBot:\n    \"\"\"Retrieves the appropriate VfsBot class for a given country.\n\n    This function searches for a matching subclass of `VfsBot` based on the\n    provided destination country code (ISO 3166-1 alpha-2).\n    If no matching class is found, an `UnsupportedCountryError` exception is raised.\n\n    Args:\n        source_country_code (str): The ISO 3166-1 alpha-2 country code where you're applying from.\n        destination_country_code (str): The ISO 3166-1 alpha-2 country code where the appointment is needed.\n\n    Returns:\n        VfsBot: An instance of the `VfsBot` subclass specific to the provided country.\n\n    Raises:\n        UnsupportedCountryError: If the provided country is not supported.\n    \"\"\"\n\n    country_lower = destination_country_code\n\n    if country_lower == \"DE\":\n        from .vfs_bot_de import VfsBotDe\n\n        return VfsBotDe(source_country_code)\n    elif country_lower == \"IT\":\n        from .vfs_bot_it import VfsBotIt\n\n        return VfsBotIt(source_country_code)\n    else:\n        raise UnsupportedCountryError(\n            f\"Country {destination_country_code} is not supported\"\n        )\n"
  },
  {
    "path": "vfs_appointment_bot/vfs_bot/vfs_bot_it.py",
    "content": "import logging\nfrom typing import Dict, List, Optional\n\nfrom playwright.sync_api import Page\n\nfrom vfs_appointment_bot.utils.date_utils import extract_date_from_string\nfrom vfs_appointment_bot.vfs_bot.vfs_bot import VfsBot\n\n\nclass VfsBotIt(VfsBot):\n    \"\"\"Concrete implementation of VfsBot for Italy (IT).\n\n    This class inherits from the base `VfsBot` class and implements\n    country-specific logic for interacting with the VFS website for Italy.\n    It overrides the following methods to handle Italy website specifics:\n\n    - `login`: Fills the login form elements with email and password.\n    - `pre_login_steps`: Rejects all cookie policies if presented.\n    - `check_for_appontment`: Performs appointment search based on provided\n        parameters and extracts available dates from the website.\n    \"\"\"\n\n    def __init__(self, source_country_code: str):\n        \"\"\"\n        Initializes a VfsBotIt instance for Italy.\n\n        This constructor sets the source country code and the destination country\n        code \"it\"(Italy). It also defines appointment parameter keys specific\n        to the destination country's VFS website.\n\n        Args:\n            source_country_code (str): The country code where you're applying from.\n        \"\"\"\n        super().__init__()\n        self.source_country_code = source_country_code\n        self.destination_country_code = \"IT\"\n        self.appointment_param_keys = [\n            \"visa_center\",\n            \"visa_category\",\n            \"visa_sub_category\",\n        ]\n\n        if self.source_country_code == \"MA\":\n            self.appointment_param_keys.append(\"payment_mode\")\n\n    def login(self, page: Page, email_id: str, password: str) -> None:\n        \"\"\"\n        Performs login steps specific to the Italy VFS website.\n\n        This method fills the email and password input fields on the login form\n        and clicks the \"Sign In\" button. It raises an exception if the login fails\n        (e.g., if the \"Start New Booking\" button is not found after login).\n\n        Args:\n            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.\n            email_id (str): The user's email address for VFS login.\n            password (str): The user's password for VFS login.\n\n        Raises:\n            Exception: If login fails due to unexpected errors or missing \"Start New Booking\" button.\n        \"\"\"\n        email_input = page.locator(\"#mat-input-0\")\n        password_input = page.locator(\"#mat-input-1\")\n\n        email_input.fill(email_id)\n        password_input.fill(password)\n\n        page.get_by_role(\"button\", name=\"Sign In\").click()\n        page.wait_for_selector(\"role=button >> text=Start New Booking\")\n\n    def pre_login_steps(self, page: Page) -> None:\n        \"\"\"\n        Performs pre-login steps specific to the Italy VFS website.\n\n        This method checks for a \"Reject All\" button for cookie policies and\n        clicks it if found.\n\n        Args:\n            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.\n        \"\"\"\n        policies_reject_button = page.get_by_role(\"button\", name=\"Reject All\")\n        if policies_reject_button is not None:\n            policies_reject_button.click()\n            logging.debug(\"Rejected all cookie policies\")\n\n    def check_for_appontment(\n        self, page: Page, appointment_params: Dict[str, str]\n    ) -> Optional[List[str]]:\n        \"\"\"\n        Checks for appointments on the Italy VFS website based on provided parameters.\n\n        This method clicks the \"Start New Booking\" button, selects the specified\n        visa center, category, and subcategory based on the `appointment_params`\n        dictionary. It then extracts the available appointment dates from the\n        website and returns them as a list. If no appointments are found, it\n        returns None.\n\n        Args:\n            page (playwright.sync_api.Page): The Playwright page object used for browser interaction.\n            appointment_params (Dict[str, str]): A dictionary containing appointment search criteria.\n\n        Returns:\n            Optional[List[str]]: A list of available appointment dates (empty list if none found),\n                including a timestamp of the check, or None if no appointments found.\n        \"\"\"\n        page.get_by_role(\"button\", name=\"Start New Booking\").click()\n\n        # Select Visa Centre\n        visa_centre_dropdown = page.wait_for_selector(\"mat-form-field\")\n        visa_centre_dropdown.click()\n        visa_centre_dropdown_option = page.wait_for_selector(\n            f'mat-option:has-text(\"{appointment_params.get(\"visa_center\")}\")'\n        )\n        visa_centre_dropdown_option.click()\n\n        # Select Visa Category\n        visa_category_dropdown = page.query_selector_all(\"mat-form-field\")[1]\n        visa_category_dropdown.click()\n        visa_category_dropdown_option = page.wait_for_selector(\n            f'mat-option:has-text(\"{appointment_params.get(\"visa_category\")}\")'\n        )\n        visa_category_dropdown_option.click()\n\n        # Select Subcategory\n        visa_subcategory_dropdown = page.query_selector_all(\"mat-form-field\")[2]\n        visa_subcategory_dropdown.click()\n        visa_subcategory_dropdown_option = page.wait_for_selector(\n            f'mat-option:has-text(\"{appointment_params.get(\"visa_sub_category\")}\")'\n        )\n        visa_subcategory_dropdown_option.click()\n\n        if self.source_country_code == \"MA\":\n            # Select Payment Mode\n            payment_mode_dropdown = page.query_selector_all(\"mat-form-field\")[3]\n            payment_mode_dropdown.click()\n            payment_mode_dropdown_option = page.wait_for_selector(\n                f'mat-option:has-text(\"{appointment_params.get(\"payment_mode\")}\")'\n            )\n            payment_mode_dropdown_option.click()\n\n        try:\n            page.wait_for_selector(\"div.alert\")\n            appointment_date_elements = page.query_selector_all(\"div.alert\")\n            appointment_dates = []\n            for appointment_date_element in appointment_date_elements:\n                appointment_date_text = appointment_date_element.text_content()\n                appointment_date = extract_date_from_string(appointment_date_text)\n                if appointment_date is not None and len(appointment_date) > 0:\n                    appointment_dates.append(appointment_date)\n            return appointment_dates\n        except Exception:\n            return None\n\n        return None\n"
  }
]