[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\nbuy_me_a_coffee: PantherPy\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\nlabels: bug\nassignees: AliRn76\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Environment:**\n - OS: [e.g. iOS]\n - Python Version [e.g. 3.11.4]\n - Panther Version [e.g. 2.4.0]\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: enhancement\nassignees: ''\n\n---\n\n**Describe the feature you'd like**\nA clear and concise description of what you want to happen.\n\n**Additional context**\nAdd any other context about the feature request here.\n"
  },
  {
    "path": ".github/workflows/pull_request.yaml",
    "content": "name: Pull Request\n\non:\n  pull_request:\n    types: [ opened, synchronize ]\n\njobs:\n\n  tests-linux:\n    name: Test Linux\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout source\n        uses: actions/checkout@v3\n\n      - name: Set up Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: '3.12'\n\n      - name: Install Requirements\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n\n      - name: Start MongoDB\n        uses: supercharge/mongodb-github-action@1.10.0\n        with:\n          mongodb-version: '4.4'\n\n      - name: Remove Build Directory\n        run: rm -rf build/\n\n      - name: Run Tests\n        run: coverage run tests\n\n      - name: Show Coverage\n        run: coverage report\n\n  tests-windows:\n    name: Test Windows\n    runs-on: windows-latest\n    needs: [ tests-linux ]\n\n    steps:\n      - name: Checkout source\n        uses: actions/checkout@v3\n\n      - name: Set up Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: '3.12'\n\n      - name: Install Requirements\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n\n      - name: Run Tests\n        run: python tests --not_mongodb --not_slow\n\n  tests-macos:\n    name: Test MacOS\n    runs-on: macos-latest\n    needs: [ tests-linux ]\n\n    steps:\n      - name: Checkout source\n        uses: actions/checkout@v3\n\n      - name: Set up Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: '3.12'\n\n      - name: Install Requirements\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n\n      - name: Run Tests\n        run: python tests --not_mongodb --not_slow\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n\n  initial-test:\n    name: Initial Test\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout source\n        uses: actions/checkout@v3\n\n      - name: Set up Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: '3.12'\n\n      - name: Install Requirements\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n\n      - name: Run Tests\n        run: python tests --not_mongodb --not_slow\n\n  tests-linux:\n    name: Test Linux Python${{ matrix.python-version }}\n    runs-on: ubuntu-latest\n    needs: [ initial-test ]\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14']\n\n    steps:\n      - name: Checkout source\n        uses: actions/checkout@v3\n\n      - name: Set up Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install Requirements\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n\n      - name: Run Tests\n        run: python tests --not_mongodb\n\n  tests-windows:\n    name: Test Windows Python${{ matrix.python-version }}\n    runs-on: windows-latest\n    needs: [ initial-test ]\n    strategy:\n      matrix:\n        python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14']\n\n    steps:\n      - name: Checkout source\n        uses: actions/checkout@v3\n\n      - name: Set up Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install Requirements\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n\n      - name: Run Tests\n        run: python tests --not_mongodb\n\n  tests-macos:\n    name: Test MacOS Python${{ matrix.python-version }}\n    runs-on: macos-latest\n    needs: [ initial-test ]\n    strategy:\n      matrix:\n        python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14']\n\n    steps:\n      - name: Checkout source\n        uses: actions/checkout@v3\n\n      - name: Set up Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install Requirements\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n\n      - name: Run Tests\n        run: python tests --not_mongodb\n\n  tests-mongodb:\n    name: Test MongoDB ${{ matrix.mongodb-version }}\n    runs-on: ubuntu-latest\n    needs: [ initial-test ]\n    strategy:\n      matrix:\n        mongodb-version: [ '4.2', '4.4', '5.0', '6.0' ]\n\n    steps:\n      - name: Checkout source\n        uses: actions/checkout@v3\n\n      - name: Set up Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: '3.11'\n\n      - name: Install Requirements\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n\n      - name: Install motor\n        run: pip install motor\n\n      - name: Start MongoDB\n        uses: supercharge/mongodb-github-action@1.10.0\n        with:\n          mongodb-version: ${{ matrix.mongodb-version }}\n\n      - name: Run Tests\n        run: python tests --mongodb\n\n  coverage:\n    name: Coverage\n    runs-on: ubuntu-latest\n    needs: [ tests-linux, tests-mongodb, tests-macos, tests-windows ]\n\n    steps:\n      - name: Checkout source\n        uses: actions/checkout@v3\n\n      - name: Set up Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: '3.12'\n\n      - name: Install Requirements\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n\n      - name: Install motor\n        run: pip install motor\n\n      - name: Start MongoDB\n        uses: supercharge/mongodb-github-action@1.10.0\n        with:\n          mongodb-version: '4.4'\n\n      - name: Remove Build Directory\n        run: rm -rf build/\n\n      - name: Run Coverage Tests\n        run: coverage run -m tests\n\n      - name: Show Coverage\n        run: coverage report\n\n      - name: Upload coverage reports to Codecov\n        uses: codecov/codecov-action@v3\n        env:\n          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n\n  build-n-publish:\n    name: Build and publish to PyPI\n    runs-on: ubuntu-latest\n    needs: [ coverage ]\n\n    steps:\n      - name: Checkout source\n        uses: actions/checkout@v3\n\n      - name: Set up Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: '3.12'\n\n      - name: Build source and wheel distributions\n        run: |\n          python -m pip install --upgrade build twine\n          python -m build\n          twine check --strict dist/*\n\n      - name: Publish distribution to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n        with:\n          password: ${{ secrets.PYPI_TOKEN }}\n\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea/\n.venv*/\n__pycache__/\npanther.egg-info/\ndist/\nexample/panther/\nlogs/\nbuild/\n*.pdb\n.ruff_cache/\ndocs/site/*\n*coverage*\n.prettierrc\n.vscode"
  },
  {
    "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.\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][homepage],\nversion 2.0, available at\nhttps://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\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Panther\n\nThank you for your interest in contributing to Panther! This document provides guidelines and instructions for setting up the development environment and contributing to the project.\n\n## Table of Contents\n\n- [Prerequisites](#prerequisites)\n- [Development Setup](#development-setup)\n- [Running Tests](#running-tests)\n- [Development Workflow](#development-workflow)\n- [Code Style](#code-style)\n- [Submitting Changes](#submitting-changes)\n- [Getting Help](#getting-help)\n\n## Prerequisites\n\nBefore you begin, ensure you have the following installed:\n\n- **Python 3.10+** (Panther requires Python 3.10 or higher)\n- **Git** for version control\n- **Docker**  for running test dependencies\n- **pip** for package management\n\n## Development Setup\n\nFollow these steps to set up your development environment:\n\n### 1. Clone the Repository\n\n```bash\ngit clone https://github.com/AliRn76/panther.git\ncd panther\n```\n\n### 2. Create a Virtual Environment\n\n```bash\n# Create virtual environment\npython3.11 -m venv .venv\n\n# Activate virtual environment\n# On Linux/macOS:\nsource .venv/bin/activate\n# On Windows:\n# .venv\\Scripts\\activate\n```\n\n### 3. Install Dependencies\n\n```bash\n# Install development dependencies\npip install -r requirements.txt\n\n# Install Panther in development mode\npip install -e .\n```\n\n## Running Tests\n\n### Prerequisites\n\nBefore running tests, you need to start the required services using Docker:\n\n```bash\n# Start MongoDB (required for database tests)\ndocker run --rm -p 27017:27017 -d --name mongo mongo\n\n# Start Redis (required for caching tests)\ndocker run --rm -p 6379:6379 -d --name redis redis\n```\n\n**Note:** Make sure Docker is running on your system before executing these commands.\n\n### Running Tests\n\n```bash\n# Run all tests\npython tests\n\n# Run specific test file\npython -m pytest tests/test_specific_file.py\n\n# Run tests with specific markers\npython tests --mongodb      # Run only MongoDB tests\npython tests --not_mongodb  # Don't run MongoDB tests\npython tests --slow         # Run only slow tests\npython tests --not_slow     # Don't run slow tests\n```\n\n### Test Coverage\n\n```bash\n# Run tests with coverage\ncoverage run tests\n\n# Generate coverage report\ncoverage report\n\n# Generate HTML coverage report\ncoverage html\n# Open htmlcov/index.html in your browser to view the report\n```\n\n## Development Workflow\n\n### Making Changes\n\n1. **Create a new branch** for your feature or fix:\n   ```bash\n   git checkout -b feature/your-feature-name\n   # or\n   git checkout -b fix/your-fix-name\n   ```\n\n2. **Make your changes** in the source code located in the `panther/` directory.\n\n3. **Test your changes**:\n   ```bash\n   # Run tests to ensure nothing is broken\n   python tests\n   \n   # Run linting\n   ruff check .\n   ```\n\n4. **Reinstall Panther** after making changes to see them immediately:\n   ```bash\n   pip install -e .\n   ```\n\n### Code Style\n\nPanther uses [Ruff](https://github.com/astral-sh/ruff) for code formatting and linting. The configuration is in `ruff.toml`.\n\n```bash\n# Format code\nruff format .\n\n# Check for linting issues\nruff check .\n\n# Fix auto-fixable issues\nruff check --fix .\n```\n\n### Testing Your Changes\n\nYou can test your changes in real-time by:\n\n1. **Modifying the installed package** (for quick testing):\n   - Edit files in `.venv/lib/python3.11/site-packages/panther/`\n   - Changes are immediately available for testing\n   - **Important:** Remember to copy your changes back to the source files before committing\n\n2. **Using development installation**:\n   - Make changes in the `panther/` directory\n   - Reinstall: `pip install -e .`\n   - Test your changes\n\n## Submitting Changes\n\n### Before Submitting\n\n1. **Ensure all tests pass**:\n   ```bash\n   python tests\n   ```\n\n2. **Check code style**:\n   ```bash\n   ruff check .\n   ruff format .\n   ```\n\n3. **Update documentation** if your changes affect public APIs or behavior.\n\n### Creating a Pull Request\n\n1. **Push your branch** to your fork:\n   ```bash\n   git push origin feature/your-feature-name\n   ```\n\n2. **Create a Pull Request** on GitHub with:\n   - Clear description of the changes\n   - Reference to any related issues\n   - Screenshots or examples if applicable\n\n3. **Wait for review** and address any feedback from maintainers.\n\n## Getting Help\n\n- **Documentation**: [PantherPy.GitHub.io](https://pantherpy.github.io)\n- **Issues**: [GitHub Issues](https://github.com/AliRn76/panther/issues)\n- **Discussions**: [GitHub Discussions](https://github.com/AliRn76/panther/discussions)\n\n## Additional Resources\n\n- **Project Structure**: Explore the `panther/` directory to understand the codebase\n- **Examples**: Check the `example/` directory for usage examples\n- **Benchmarks**: Review `benchmark.txt` for performance data\n\n---\n\nThank you for contributing to Panther! 🐾\n"
  },
  {
    "path": "LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2022, Ali Rajab nezhad\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "[![PyPI](https://img.shields.io/pypi/v/panther?label=PyPI)](https://pypi.org/project/panther/) [![PyVersion](https://img.shields.io/pypi/pyversions/panther.svg)](https://pypi.org/project/panther/) [![codecov](https://codecov.io/github/AliRn76/panther/graph/badge.svg?token=YWFQA43GSP)](https://codecov.io/github/AliRn76/panther) [![Downloads](https://static.pepy.tech/badge/panther/month)](https://pepy.tech/project/panther) [![license](https://img.shields.io/github/license/alirn76/panther.svg)](https://github.com/alirn76/panther/blob/main/LICENSE)\n\n<div align=\"center\">\n  <img src=\"https://github.com/AliRn76/panther/raw/master/docs/docs/images/logo-vertical.png\" alt=\"Panther Logo\" width=\"450\">\n  \n  # Panther \n  \n  **A Fast & Friendly Web Framework for Building Async APIs with Python 3.10+**\n  \n  [📚 Documentation](https://pantherpy.github.io)\n</div>\n\n---\n\n## 🐾 Why Choose Panther?\n\nPanther is designed to be **fast**, **simple**, and **powerful**. Here's what makes it special:\n\n- **One of the fastest Python frameworks** available ([Benchmark](https://www.techempower.com/benchmarks/#section=data-r23&l=zijzen-pa7&c=4))\n- **File-based database** ([PantherDB](https://pypi.org/project/pantherdb/)) - No external database setup required\n- **Document-oriented ODM** - Supports MongoDB & PantherDB with familiar syntax\n- **API caching system** - In-memory and Redis support\n- **OpenAPI** - Auto-generated API documentation with multiple UI options\n- **WebSocket support** - Real-time communication out of the box\n- **Authentication & Permissions** - Built-in security features\n- **Background tasks** - Handle long-running operations\n- **Middleware & Throttling** - Extensible and configurable\n\n---\n\n## Quick Start\n\n### Installation\n\n```bash\npip install panther\n```\n\n- Create a `main.py` file with one of the examples below.\n\n### Your First API\n\nHere's a simple REST API endpoint that returns a \"Hello World\" message:\n\n```python\nfrom datetime import datetime, timedelta\nfrom panther import status, Panther\nfrom panther.app import GenericAPI\nfrom panther.openapi.urls import url_routing as openapi_url_routing\nfrom panther.response import Response\n\nclass HelloAPI(GenericAPI):\n    # Cache responses for 10 seconds\n    cache = timedelta(seconds=10)\n    \n    def get(self):\n        current_time = datetime.now().isoformat()\n        return Response(\n            data={'message': f'Hello from Panther! 🐾 | {current_time}'},\n            status_code=status.HTTP_200_OK\n        )\n\n# URL routing configuration\nurl_routing = {\n    '/': HelloAPI,\n    'docs/': openapi_url_routing,  # Auto-generated API docs\n}\n\n# Create your Panther app\napp = Panther(__name__, configs=__name__, urls=url_routing)\n```\n\n### WebSocket Echo Server\n\nHere's a simple WebSocket echo server that sends back any message it receives:\n\n```python\nfrom panther import Panther\nfrom panther.app import GenericAPI\nfrom panther.response import HTMLResponse\nfrom panther.websocket import GenericWebsocket\n\nclass EchoWebsocket(GenericWebsocket):\n    async def connect(self, **kwargs):\n        await self.accept()\n        await self.send(\"Connected to Panther WebSocket!\")\n    \n    async def receive(self, data: str | bytes):\n        # Echo back the received message\n        await self.send(f\"Echo: {data}\")\n\nclass WebSocketPage(GenericAPI):\n    def get(self):\n        template = \"\"\"\n        <h2>🐾 Panther WebSocket Echo Server</h2>\n        <input id=\"msg\"><button onclick=\"s.send(msg.value)\">Send</button>\n        <ul id=\"log\"></ul>\n        <script>\n            const s = new WebSocket('ws://127.0.0.1:8000/ws');\n            s.onmessage = e => log.innerHTML += `<li><- ${msg.value}</li><li>-> ${e.data}</li>`;\n        </script>\n        \"\"\"\n        return HTMLResponse(template)\n\nurl_routing = {\n    '': WebSocketPage,\n    'ws': EchoWebsocket,\n}\napp = Panther(__name__, configs=__name__, urls=url_routing)\n```\n\n### Run Your Application\n\n1. **Start the development server**\n   ```shell\n   $ panther run main:app\n   ```\n   \n2. **Test your application**\n    - For the _API_ example: Visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/) to see the \"Hello World\" response\n    - For the _WebSocket_ example: Visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/) and send a message.\n\n---\n\n## 🙏 Acknowledgments\n\n<div align=\"center\">\n  <p>Supported by</p>\n  <a href=\"https://drive.google.com/file/d/17xe1hicIiRF7SQ-clg9SETdc19SktCbV/view?usp=sharing\">\n    <img alt=\"JetBrains\" src=\"https://github.com/AliRn76/panther/raw/master/docs/docs/images/jb_beam_50x50.png\">\n  </a>\n</div>\n\n---\n\n<div align=\"center\">\n  <p>⭐️ If you find Panther useful, please give it a star!</p>\n</div>\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nUse this section to tell people about which versions of your project are\ncurrently being supported with security updates.\n\n| Version | Supported          |\n| ------- | ------------------ |\n| > 1.7.x   | :white_check_mark: |\n| < 1.7   | :x:                |\n\n## Reporting a Vulnerability\n\nIt is an open-source project and we would be happy if you report an issue if you found any vulnerabilities 😃🖐️\n"
  },
  {
    "path": "benchmark.txt",
    "content": "\"\"\"\nCheckout profiler.py for more information\n\"\"\"\n\nClock type: CPU\nOrdered by: totaltime, desc\n\nname                                                                                                  ncall       tsub        ttot        tavg        \n\n/home/ali/dev/panther/panther/main.py:78 Panther.__call__                                             1001        0.012665    0.811156    0.000810\n/home/ali/dev/panther/panther/main.py:161 Panther.handle_http                                         1000        0.035138    0.798476    0.000798\n/home/ali/dev/panther/panther/response.py:167 Response.send                                           1000        0.009427    0.473242    0.000473\n/home/ali/dev/panther/panther/response.py:161 Response.send_headers                                   1000        0.012985    0.287794    0.000288\n/home/ali/dev/panther/panther/middlewares/base.py:15 TestMiddleware.__call__                          1000        0.005173    0.239159    0.000239\n/home/ali/dev/panther/panther/main.py:142 handle_http_endpoint                                        1000        0.021648    0.233986    0.000234\n/home/ali/dev/panther/panther/response.py:164 Response.send_body                                      1000        0.009923    0.176022    0.000176\n/home/ali/dev/panther/panther/app.py:120 wrapper                                                      1000        0.008120    0.133374    0.000133\n/home/ali/dev/panther/panther/app.py:136 API.handle_endpoint                                          1000        0.042525    0.125255    0.000125\n/home/ali/dev/panther/panther/response.py:124 Response.bytes_headers                                  1000        0.024477    0.062525    0.000063\n/home/ali/dev/panther/panther/response.py:67 Response.__init__                                        1000        0.019294    0.059479    0.000059\n/home/ali/dev/panther/panther/routings.py:133 find_endpoint                                           1000        0.026393    0.043979    0.000044\n/home/ali/dev/panther/panther/request.py:40 Request.read_body                                         1000        0.011115    0.036803    0.000037\n/home/ali/dev/panther/panther/response.py:135 prepare_data                                            2000/1000   0.023992    0.031797    0.000016\n/home/ali/dev/panther/panther/base_request.py:129 Request.collect_path_variables                      1000        0.019701    0.031360    0.000031\n/home/ali/dev/panther/panther/response.py:116 Response.headers                                        1000        0.009755    0.025101    0.000025\n/home/ali/dev/panther/panther/response.py:107 Response.body                                           2000        0.011792    0.024099    0.000012\n/home/ali/dev/panther/panther/base_request.py:139 Request.clean_parameters                            1000        0.007809    0.011867    0.000012\n/home/ali/dev/panther/panther/request.py:14 Request.__init__                                          1000        0.008140    0.010023    0.000010\n/home/ali/dev/panther/panther/response.py:154 check_status_code                                       1000        0.003490    0.004959    0.000005\n/home/ali/dev/panther/panther/request.py:19 Request.method                                            3000        0.004952    0.004952    0.000002\n/home/ali/dev/panther/panther/middlewares/base.py:12 TestMiddleware.__init__                          1000        0.004111    0.004111    0.000004\n/home/ali/dev/panther/panther/base_request.py:109 Request.path                                        2000        0.003424    0.003424    0.000002\n/home/ali/dev/panther/panther/base_request.py:88 Request.__init__                                     1000        0.001883    0.001883    0.000002\n/home/ali/dev/panther/panther/app.py:189 API.handle_authentication                                    1000        0.001752    0.001752    0.000002\n/home/ali/dev/panther/panther/response.py:131 Response.headers                                        1000        0.001583    0.001583    0.000002\n/home/ali/dev/panther/panther/events.py:27 run_startups                                               1           0.000001    0.000001    0.000001"
  },
  {
    "path": "docs/docs/admin_panel.md",
    "content": "> **⚠️ WARNING: The Panther Admin Panel is currently in development. Do NOT use it in production environments!**\n> \n> **Contributions, feedback, and ideas are welcome!**\n\n# Panther Admin Panel\n\nPanther provides a built-in admin panel that allows you to easily manage your database models through a web interface.\n\n## Enabling the Admin Panel\n\nTo enable the admin panel in your project, follow these steps:\n\n### 1. Add the Admin Panel URLs\n\nFirst, ensure your main URL configuration includes the admin panel routes. e.g. open your `core/urls.py` and add the following:\n\n```python title=\"core/urls.py\"\nfrom panther.panel.urls import url_routing as panel_url_routing\n\nurl_routing = {\n    'panel/': panel_url_routing,\n    # ... other routes ...\n}\n```\n\n> **Note:** If you are using a different file for your URL routing, adjust the import and assignment accordingly.\n\n### 2. Update Your Configs (if needed)\n\nIf your project uses a custom configuration file for URLs, make sure it points to your updated URL routing. For example, in `core/configs.py`:\n\n```python title=\"core/configs.py\"\nURLs = 'core.urls.url_routing'\n```\n\n### 3. Run Your Application\n\nStart your Panther application as usual:\n\n```bash\npanther run main:app\n```\n\n### 4. Access the Admin Panel\n\nOpen your browser and navigate to:\n\n[http://127.0.0.1:8000/panel/](http://127.0.0.1:8000/panel/)\n\nYou should see the Panther Admin Panel interface, where you can manage your database models. \n\n### 5. Create an Admin User\n\nTo access the admin panel, you need at least one user account. You can create a user using the following command:\n\n```bash\npanther createuser main.py\n```\n\nReplace `main.py` with the path to your main application file if it is different. This command will create a new user based on your `USER_MODEL` (by default, `panther.db.models.BaseUser`).\n\nOnce the user is created, you can log in to the admin panel using the credentials you set during user creation. "
  },
  {
    "path": "docs/docs/api.md",
    "content": "# Panther API Guide\n\nThis guide assumes you have successfully set up your project and created your first CRUD following the [First CRUD](first_crud.md) guide.\n\nIn this guide, we discuss the features and structure of Panther's API system, including authentication, permissions, caching, throttling, middlewares, and more.\n\n---\n\n## API Request Flow\n\nThe typical flow of an API request in Panther is as follows:\n\n```\nMiddlewares\n├── Method\n├── Authentication\n├── Permissions\n├── Throttling\n├── Validate Input\n├── Get Response From Cache\n├── Call Endpoint\n├── Set Response To Cache\nMiddlewares\n```\n\n---\n\n## Input Model\n\nYou can validate incoming data using the `input_model` parameter. Pass a serializer to it, and Panther will send `request.data` to this serializer, placing the validated data in `request.validated_data`. As a result, `request.validated_data` will be an instance of your serializer.\n\n> Note: `request.data` is validated only for 'POST', 'PUT', and 'PATCH' methods.\n\n??? question \"How do serializers work in Panther?\"\n    Refer to [Serializer](serializer.md) to learn more about serializers.\n\n```python title=\"app/serializers.py\" linenums=\"1\"\nfrom panther.serializer import ModelSerializer\n\nclass UserInputSerializer(ModelSerializer):\n    ...\n```\n=== \"Function-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther import status\n    from panther.app import API\n    from panther.request import Request\n    from panther.response import Response\n    \n    from app.serializers import UserInputSerializer\n    \n\n    @API(input_model=UserInputSerializer)\n    async def user_api(request: Request):\n        request.validated_data  # This is now available\n        ...\n    ```\n\n=== \"Class-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther import status\n    from panther.app import GenericAPI\n    from panther.response import Response\n    \n    from app.serializers import UserInputSerializer\n    \n    \n    class UserAPI(GenericAPI):\n        input_model = UserInputSerializer\n    \n        async def get(self, request: Request):\n            request.validated_data  # This is now available\n            ...\n    ```\n\n---\n\n## Output Model\n\nUse the `output_model` parameter to automatically serialize your API response data using a specified serializer. This ensures that the response structure is consistent and validated.\n\n**Example Serializer:**\n\n```python title=\"app/serializers.py\" linenums=\"1\"\nfrom panther.serializer import ModelSerializer\n\nclass UserSerializer(ModelSerializer):\n    ...\n```\n\n=== \"Function-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther.app import API\n    from app.serializers import UserSerializer\n    \n    @API(output_model=UserSerializer)\n    async def user_api():\n        ...\n    ```\n\n=== \"Class-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther.app import GenericAPI\n    from app.serializers import UserSerializer\n    \n    class UserAPI(GenericAPI):\n        output_model = UserSerializer\n        ...\n    ```\n\n> **Tip:** Use `output_model` to ensure your API always returns data in the expected format. For OpenAPI documentation, see the `output_schema` section.\n\n---\n\n## Authentication\n\nTo ensure that each request contains a valid authentication header, use the `auth` parameter. \n\n- The `auth` parameter can be an any async function or a class with an async `__call__` method.\n- If you set `auth`, Panther will use your specified authentication class or function. \n- If you do not set `auth`, Panther will use the default `AUTHENTICATION` from your config **only if the request contains an authorization header**. \n- If there is no authorization header in the request, authentication is bypassed, `request.user` will be `None` and you must rely on permissions to check the user and their authorization.\n\n??? question \"How do authentications work in Panther?\"\n    Refer to [Authentications](authentications.md) to learn more about authentications.\n\n=== \"Function-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther import status\n    from panther.app import API\n    from panther.request import Request\n    from panther.response import Response\n    from app.authentications import MyAuthenticationClass\n    \n    @API(auth=MyAuthenticationClass)  # You can also use a function\n    async def user_api(request: Request):\n        user = request.user\n        return Response(data=user, status_code=status.HTTP_200_OK)\n    ```\n\n\n=== \"Class-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther import status\n    from panther.app import GenericAPI\n    from panther.response import Response\n    from app.authentications import MyAuthenticationClass\n    \n    class UserAPI(GenericAPI):\n        auth = MyAuthenticationClass  # You can also use a function\n    \n        async def get(self, request: Request):\n            user = request.user\n            return Response(data=user, status_code=status.HTTP_200_OK)\n    ```\n\n---\n\n## Method\n\nYou can specify which HTTP methods are allowed for an endpoint by setting `methods` in function-based APIs. Only the following methods are supported: `['GET', 'POST', 'PUT', 'PATCH', 'DELETE']`.\n\n> If a method is not allowed, a 405 status code will be returned.\n\n=== \"Function-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther.app import API\n    from panther.request import Request\n    \n    @API(methods=['GET', 'POST'])\n    async def user_api(request: Request):\n        match request.method:\n            case 'GET':\n                ...\n            case 'POST':\n                ...\n    ```\n\n=== \"Class-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther.app import GenericAPI\n    \n    class UserAPI(GenericAPI):  # (1)!\n        async def get(self):\n            ...\n    \n        async def post(self):\n            ...\n    ```\n\n    1. Now this class only accepts `GET` and `POST` requests.\n\n---\n\n## Permissions\n\nYou can implement your authorization logic using permission classes or functions. Any async function or class with an async `__call__` method can be used as a permission. Panther will call each permission (asynchronously).\n\nPass a list of permission callables to your API using the `permissions` parameter. If you pass a single permission, it will be automatically wrapped in a list.\n\n> Each permission must be async (either an async function or a class with an async `__call__`).\n\n**Example Permission Function:**\n\n```python title=\"app/permissions.py\" linenums=\"1\"\nfrom panther.request import Request\n\nasync def custom_permission(request: Request) -> bool:\n    return True\n```\n\n**Example Permission Class:**\n\n```python title=\"app/permissions.py\" linenums=\"1\"\nfrom panther.permissions import BasePermission\nfrom panther.request import Request\n\nclass CustomPermission(BasePermission):\n    async def __call__(self, request: Request) -> bool:\n        return True\n```\n\n=== \"Function-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther.app import API\n    from app.permissions import custom_permission, CustomPermission\n    \n    @API(permissions=[custom_permission, CustomPermission])\n    async def user_api():\n        ...\n    ```\n    Or, if you have only one permission:\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther.app import API\n    from app.permissions import custom_permission\n    \n    @API(permissions=custom_permission)\n    async def user_api():\n        ...\n    ```\n    Panther will treat it as a list internally.\n\n=== \"Class-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther.app import GenericAPI\n    from app.permissions import CustomPermission\n    \n    class UserAPI(GenericAPI):\n        permissions = [CustomPermission]\n        ...\n    ```\n    Or, if you have only one permission:\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther.app import GenericAPI\n    from app.permissions import custom_permission\n    \n    class UserAPI(GenericAPI):\n        permissions = custom_permission\n        ...\n    ```\n    Panther will treat it as a list internally.\n\n---\n\n## Cache\n\nResponses can be cached for a specific amount of time per request or IP. Caching is only applied to `GET` requests. The response's headers, data, and status code will be cached.\n\nThe cache is stored in Redis (if connected) or in memory. The cache key is based on the user ID or IP, request path, query parameters, and validated data:\n\n```\n'user_id or ip - request.path - hash of query param - request.validated_data'\n```\n\nThe value of `cache` should be an instance of `datetime.timedelta()`.\n\n=== \"Function-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from datetime import timedelta\n    from panther.app import API\n    \n    @API(cache=timedelta(minutes=10))\n    async def user_api():\n        ...\n    ```\n\n=== \"Class-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from datetime import timedelta\n    from panther.app import GenericAPI\n    \n    class UserAPI(GenericAPI):\n        cache = timedelta(minutes=10)\n        ...\n    ```\n\n---\n\n## Throttling\n\nYou can throttle requests using the `Throttle` class, either globally via the `THROTTLING` config or per API. The `Throttle` class has two fields: `rate` and `duration`.\n\nIf a user exceeds the allowed number of requests (`rate`) within the specified `duration`, they will receive a `429 Too Many Requests` response and be banned for the duration.\n\n> When you set `throttling` on your API, it takes precedence over the default `THROTTLING`, and the default `THROTTLING` will not be executed.\n\n### Setting Default Throttling\n\n```python\nfrom datetime import timedelta\nfrom panther.throttling import Throttle\n\n# Users can only make 5 requests every minute\nTHROTTLING = Throttle(rate=5, duration=timedelta(minutes=1))\n```\n\n### Throttling Per API\n\n=== \"Function-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from datetime import timedelta\n    from panther.app import API\n    from panther.throttling import Throttle\n    \n    @API(throttling=Throttle(rate=5, duration=timedelta(minutes=1)))\n    async def user_api():\n        ...\n    ```\n\n=== \"Class-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from datetime import timedelta\n    from panther.app import GenericAPI\n    from panther.throttling import Throttle\n    \n    class UserAPI(GenericAPI):\n        throttling = Throttle(rate=5, duration=timedelta(minutes=1))\n        ...\n    ```\n\n### Customization\n\nThrottling works with `request.user.id` or `request.client.ip`. You can customize its behavior by overriding `build_cache_key()`:\n\n```python title=\"app/throttlings.py\" linenums=\"1\"\nfrom panther.request import Request\nfrom panther.throttling import Throttle\n\nclass CustomThrottle(Throttle):\n    def build_cache_key(self, request: Request) -> str:\n        ...\n```\n\n---\n\n## Middlewares\n\nYou can pass custom middlewares to specific APIs.\n\n**Example Middleware:**\n\n```python title=\"app/middlewares.py\" linenums=\"1\"\nfrom panther.middlewares.base import HTTPMiddleware\nfrom panther.request import Request\nfrom panther.response import Response\n\nclass CustomMiddleware(HTTPMiddleware):\n    async def __call__(self, request: Request) -> Response:\n        print('Hi')\n        response = await self.dispatch(request=request)\n        print('Bye')\n        return response\n```\n\n=== \"Function-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther.app import API\n    from app.middlewares import CustomMiddleware \n    \n    @API(middlewares=[CustomMiddleware])\n    async def user_api():\n        ...\n    ```\n\n=== \"Class-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther.app import GenericAPI\n    from app.middlewares import CustomMiddleware \n    \n    class UserAPI(GenericAPI):\n        middlewares = [CustomMiddleware]\n        ...\n    ```\n\n??? question \"How do middlewares work in Panther?\"\n    Refer to [Middlewares](middlewares.md) to learn more about middlewares.\n\n---\n\n## Output Schema\n\nThe `output_schema` attribute is used when generating OpenAPI (Swagger) documentation. \nIt should be an instance of `panther.openapi.OutputSchema`, which specifies the desired response data structure and status code.\n\n**Example Serializer:**\n\n```python title=\"app/serializers.py\" linenums=\"1\"\nfrom panther.serializer import ModelSerializer\n\nclass UserSerializer(ModelSerializer):\n    ...\n```\n\n=== \"Function-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther import status\n    from panther.app import API\n    from panther.openapi import OutputSchema\n    from app.serializers import UserSerializer\n    \n    @API(output_schema=OutputSchema(status_code=status.HTTP_201_CREATED, model=UserSerializer))\n    async def user_api():\n        ...\n    ```\n\n=== \"Class-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther.app import GenericAPI\n    from panther.openapi import OutputSchema\n    from app.serializers import UserSerializer\n    \n    class UserAPI(GenericAPI):\n        output_schema = OutputSchema(status_code=status.HTTP_201_CREATED, model=UserSerializer)\n        ...\n    ```\n\n??? question \"How does OpenAPI work in Panther?\"\n    Refer to [OpenAPI](open_api.md) to learn more about OpenAPI.\n\n---\n\n## File Handling\n\nPanther provides built-in support for file uploads through the `File` and `Image` classes.\n\n!!! tip \"Comprehensive File Handling Guide\"\n    For detailed information about file handling, including advanced features, best practices, and troubleshooting, see the dedicated [File Handling](file_handling.md) documentation.\n\n```python title=\"app/apis.py\" linenums=\"1\"\nfrom panther.app import API\nfrom panther.db import Model\nfrom panther.file_handler import File\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.serializer import ModelSerializer\n\nclass FileUpload(Model):\n    file: File\n    description: str\n\nclass FileUploadSerializer(ModelSerializer):\n    class Config:\n        model = FileUpload\n        fields = '*'\n\n@API(input_model=FileUploadSerializer)\nasync def upload_file(request: Request):\n    file_data = request.validated_data\n    file = file_data.file\n    \n    # Save file to disk\n    saved_path = file.save(\"uploads/\")\n    \n    return Response(data={\n        \"message\": \"File uploaded successfully\",\n        \"saved_path\": saved_path\n    })\n```\n"
  },
  {
    "path": "docs/docs/authentications.md",
    "content": "# Authentication in Panther\n\nAuthentication in Panther ensures that only authorized users can access your APIs and WebSocket connections. You can configure which authentication class to use in your `configs`.\n\n---\n\n## How Authentication Works\n\n- The `auth` parameter for an API or WebSocket should be an async function or a class with an async `__call__` method. Panther will use this callable to verify the user.\n- If you do not set `auth`, Panther will use the default `AUTHENTICATION` from your config **only if the request contains an authorization header**. If there is no authorization header, authentication is bypassed and `request.user` will be `None`.\n- If authentication fails, Panther raises `HTTP_401_UNAUTHORIZED`.\n- The authenticated user is available as:\n  - `request.user` in API views\n  - `self.user` in WebSocket connections\n\n> **Note:** When authentication is bypassed (no authorization header), `request.user` will be `None` and you must rely on permissions to check the user and their authorization.\n\n---\n\n## Built-in Authentication Classes\n\nPanther provides three built-in authentication classes, all based on JWT (JSON Web Token):\n\n### 1. JWT Authentication\n\n- Retrieves the JWT token from the `Authorization` header in the request.\n- Expects the header format: `Authorization: Bearer <token>`\n- Decodes the token, validates it, and fetches the corresponding user.\n- By default, uses `panther.db.models.BaseUser` as the user model unless you set `USER_MODEL` in your configs.\n- Handles token revocation if Redis is connected (for logout and refresh scenarios).\n\n#### Example usage\n```python\nAUTHENTICATION = 'panther.authentications.JWTAuthentication'\n```\n\n#### JWT Configuration\nYou can customize JWT behavior by setting `JWT_CONFIG` in your configs. Example:\n\n```python title=\"core/configs.py\" linenums=\"1\"\nfrom datetime import timedelta\nfrom panther.utils import load_env  \nfrom pathlib import Path\n\nBASE_DIR = Path(__name__).resolve().parent  \nenv = load_env(BASE_DIR / '.env')\n\nSECRET_KEY = env['SECRET_KEY']\n\nJWT_CONFIG = {\n    'key': SECRET_KEY,              # Secret key for signing tokens (default: `SECRET_KEY`)\n    'algorithm': 'HS256',           # Algorithm used for JWT (default: `'HS256'`)\n    'life_time': timedelta(days=2), # Access token lifetime (default: `timedelta(days=1)`)\n    'refresh_life_time': timedelta(days=10), # Refresh token lifetime (default: `2 * life_time`)\n}\n```\n\n---\n\n### 2. QueryParam JWT Authentication\n\n- Works like `JWTAuthentication`, but expects the token in the query parameters instead of headers.\n- Useful for WebSocket authentication or scenarios where headers are not available.\n- Pass the token as a query parameter:\n  - Example: `https://example.com?authorization=<jwt_token>`\n\n#### Example usage\n```python\nAUTHENTICATION = 'panther.authentications.QueryParamJWTAuthentication'\n```\n\n---\n\n### 3. Cookie JWT Authentication\n\n- Works like `JWTAuthentication`, but expects the token in cookies.\n- Looks for `access_token` in cookies for authentication.\n- Optionally, can use `refresh_token` in cookies for token refresh.\n- Pass the token in cookies:\n  - Example: `Cookies: access_token=<jwt_token>`\n\n#### Example usage\n```python\nAUTHENTICATION = 'panther.authentications.CookieJWTAuthentication'\n```\n\n---\n\n## WebSocket Authentication\n\nFor WebSocket connections, it is recommended to use `QueryParamJWTAuthentication` since headers are not always available. To enable this, set the following in your configs:\n\n```python\nWS_AUTHENTICATION = 'panther.authentications.QueryParamJWTAuthentication'\n```\n\n---\n\n## Custom Authentication\n\nYou can implement your own authentication logic by either:\n- Inheriting from `panther.authentications.BaseAuthentication` and implementing an async `__call__` method (recommended for consistency), or\n- Providing any async function or class with an async `__call__` method.\n\n### Steps to create a custom authentication class:\n1. **Inherit from `BaseAuthentication` and implement async `__call__`**\n   ```python\n   from panther.request import Request\n   from panther.exceptions import AuthenticationAPIError\n   from panther.authentications import BaseAuthentication\n\n   class CustomAuthentication(BaseAuthentication):\n       async def __call__(self, request: Request):\n           # Your authentication logic here\n           # Return an instance of USER_MODEL (default: BaseUser)\n           # Or raise AuthenticationAPIError on failure\n           ...\n   ```\n   Or as a function or plain class:\n   ```python\n   async def custom_authentication(request: Request):\n       # Your authentication logic here\n       # Return an instance of USER_MODEL (default: BaseUser)\n       # Or raise AuthenticationAPIError on failure\n       ...\n   ```\n2. **Configure your custom authentication class or function in your configs (if you want it as default):**\n   ```python\n   AUTHENTICATION = 'project_name.core.authentications.CustomAuthentication'\n   ```\n3. **Or set it per API using the `auth` parameter:**\n   ```python\n   @API(auth=CustomAuthentication)\n   async def my_api(request: Request):\n       ...\n   ```\n\n---\n\n## Error Handling\n- If authentication fails, raise `panther.exceptions.AuthenticationAPIError` with an appropriate message.\n- Panther will automatically handle and return a 401 Unauthorized response.\n\n---\n\n## Summary\n- Choose and configure the appropriate authentication class or function for your use case.\n- Use JWT configuration options to control token behavior.\n- For WebSocket, prefer query parameter-based authentication.\n- Implement custom authentication as an async function or a class with an async `__call__` method.\n- If authentication is bypassed, `request.user` will be `None` and you must use permissions to check the user and their authorization.\n"
  },
  {
    "path": "docs/docs/background_tasks.md",
    "content": "# Background Tasks in Panther\n\nPanther can run background tasks in a separate thread at startup if you set `BACKGROUND_TASKS` to `True` in your configuration.\n\n## Quick Start\n\n1. **Enable background tasks**  \n   In your `configs`, add:\n   ```python\n   BACKGROUND_TASKS = True\n   ```\n\n2. **Create and submit a task**  \n    ```python linenums=\"1\"\n    from panther.background_tasks import BackgroundTask\n    \n    def do_something(name: str, age: int):\n       print(f\"{name} is {age} years old.\")\n    \n    BackgroundTask(do_something, name='Ali', age=26).submit()\n    ```\n    - You must call `.submit()` to add the task to the queue.\n      - The task function can be synchronous or asynchronous.\n\n---\n\n## Task Options\n\n### 1. Interval\n\nControl how many times a task runs:\n\n```python\nBackgroundTask(do_something, name='Ali', age=26).interval(3).submit()\n```\n\n- By default, tasks run once (`interval=1`).\n- Use `interval(-1)` for infinite runs.\n- Each interval is separated by the schedule you set (see below).\n\n### 2. Scheduling\n\nYou can schedule tasks to run at specific intervals:\n\n- **Every N seconds/minutes/hours/days/weeks:**\n    ```python\n    BackgroundTask(do_something, name='Ali', age=26).every_seconds(10).submit()\n    BackgroundTask(do_something, name='Ali', age=26).every_minutes(5).submit()\n    BackgroundTask(do_something, name='Ali', age=26).every_hours(2).submit()\n    BackgroundTask(do_something, name='Ali', age=26).every_days(1).submit()\n    BackgroundTask(do_something, name='Ali', age=26).every_weeks(1).submit()\n    ```\n    - Default value for each is `1` (e.g., every 1 minute).\n\n- **Custom values:**  \n  You can pass a custom value to any of the above, e.g., `every_days(4)` runs every 4 days.\n\n### 3. Time of Day\n\nRun a task at a specific time:\n\n```python linenums=\"1\"\nfrom datetime import time\nfrom panther.background_tasks import BackgroundTask\n\nBackgroundTask(do_something, name='Ali', age=26)\\\n    .every_days()\\\n    .at(time(hour=8, minute=0))\\\n    .submit()\n```\n\n- The task will run when the system time matches the specified hour, minute, and second.\n\n### 4. Day of Week\n\nRun a task on a specific day of the week:\n\n```python linenums=\"1\"\nfrom datetime import time\nfrom panther.background_tasks import BackgroundTask, WeekDay\n\nBackgroundTask(do_something, name='Ali', age=26)\\\n    .every_weeks(2)\\\n    .on(WeekDay.SUNDAY)\\\n    .at(time(hour=8))\\\n    .submit()\n```\n\n- Valid days: `WeekDay.MONDAY`, `WeekDay.TUESDAY`, `WeekDay.WEDNESDAY`, `WeekDay.THURSDAY`, `WeekDay.FRIDAY`, `WeekDay.SATURDAY`, `WeekDay.SUNDAY`.\n\n---\n\n## Passing Arguments\n\nYou can pass arguments to your task function as positional or keyword arguments:\n\n```python\nBackgroundTask(do_something, name='Ali', age=26)\nBackgroundTask(do_something, 'Ali', age=26)\nBackgroundTask(do_something, 'Ali', 26)\n```\n\n---\n\n## Important Notes & Best Practices\n\n- **Task function** can be synchronous or asynchronous.\n- **You must call `.submit()`** to add the task to the background queue.\n- **Default interval** is 1 (runs once). Use `.interval(-1)` for infinite runs.\n- If you try to add a task before `BACKGROUND_TASKS` is enabled, it will be ignored and a warning will be logged.\n- Each task runs in its own thread when triggered.\n- Tasks are checked every second for their schedule.\n\n---\n\n## Example: Task\n\n```python linenums=\"1\"\nimport datetime\nfrom panther.background_tasks import BackgroundTask\n\nasync def hello(name: str):\n    print(f'Hello {name}')\n\n# Run 2 times, every 5 seconds\nBackgroundTask(hello, 'Ali').interval(2).every_seconds(5).submit()\n\n# Run forever, every day at 08:00\nBackgroundTask(hello, 'Saba').interval(-1).every_days().at(datetime.time(hour=8)).submit()\n```\n"
  },
  {
    "path": "docs/docs/cors.md",
    "content": "# CORSMiddleware\n\n`CORSMiddleware` is a middleware for Panther applications that handles Cross-Origin Resource Sharing (CORS). It\nautomatically adds the appropriate CORS headers to all HTTP responses based on your configuration, and handles\npreflight (OPTIONS) requests.\n\n## Purpose\n\nCORS is a security feature implemented by browsers to restrict web applications running on one origin from interacting\nwith resources from a different origin. `CORSMiddleware` makes it easy to configure and manage CORS policies in your\nPanther application.\n\n## Configuration Options\n\nSet the following variables in your Panther config file (e.g., `core/configs.py`) to control CORS behavior:\n\n| Config Variable     | Type      | Description                                                          | Default                                                |\n|---------------------|-----------|----------------------------------------------------------------------|--------------------------------------------------------|\n| _ALLOW_ORIGINS_     | list[str] | List of allowed origins. Use `[\"*\"]` to allow all origins.           | `[\"*\"]`                                                |\n| _ALLOW_METHODS_     | list[str] | List of allowed HTTP methods.                                        | `[\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"OPTIONS\"]` |\n| _ALLOW_HEADERS_     | list[str] | List of allowed request headers. Use `[\"*\"]` to allow all headers.   | `[\"*\"]`                                                |\n| _ALLOW_CREDENTIALS_ | bool      | Whether to allow credentials (cookies, authorization headers, etc.). | `False`                                                |\n| _EXPOSE_HEADERS_    | list[str] | List of headers that can be exposed to the browser.                  | `[]`                                                   |\n| _CORS_MAX_AGE_      | int       | Number of seconds browsers can cache preflight responses.            | `600`                                                  |\n\n## Usage\n\n1. Set the desired CORS config variables in your config file.\n2. Add `'panther.middlewares.cors.CORSMiddleware'` to your `MIDDLEWARES` list.\n\n### Example Configuration\n\n```python title=\"e.g. core/configs.py\"\nALLOW_ORIGINS = [\"https://example.com\", \"https://another.com\"]\nALLOW_METHODS = [\"GET\", \"POST\"]\nALLOW_HEADERS = [\"Content-Type\", \"Authorization\"]\nALLOW_CREDENTIALS = True\nEXPOSE_HEADERS = [\"X-Custom-Header\"]\nCORS_MAX_AGE = 3600\n\nMIDDLEWARES = [\n    # ... other middlewares ...\n    'panther.middlewares.cors.CORSMiddleware',\n]\n```\n\n## How It Works\n\n- For every request, the middleware adds the appropriate CORS headers to the response.\n- For preflight (OPTIONS) requests, it returns a 204 response with the necessary headers.\n- The headers are set based on your configuration, with sensible defaults if not specified.\n\n## Notes\n\n- If you set `ALLOW_ORIGINS = [\"*\"]`, all origins are allowed.\n- If you set `ALLOW_HEADERS = [\"*\"]`, all headers are allowed.\n- If `ALLOW_CREDENTIALS` is `True`, the `Access-Control-Allow-Credentials` header is set to `true`.\n- If you specify `EXPOSE_HEADERS`, those headers will be exposed to the browser.\n\nFor more details, see the source code in `panther/middlewares/cors.py`.\n\n---\n\nFor a deeper understanding of CORS, you may also want to check\nthe [MDN Web Docs CORS Guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS).\n"
  },
  {
    "path": "docs/docs/database.md",
    "content": "# Database Support in Panther\n\nPanther natively supports two databases: `MongoDB` and `PantherDB`. However, you can also define your own custom database connections and queries.\n\n---\n\n## Configuration Structure\n\nDefine your database configuration in the following format:\n\n```python\nDATABASE = {\n    'engine': {\n        'class': 'address of engine',\n        'arg1': 'value1',\n        # ... additional arguments ...\n    },\n    'query': 'address of query class',  # Optional\n}\n```\n\n### Notes\n- **Built-in supported engines:**\n  - `panther.db.connections.PantherDBConnection`\n  - `panther.db.connections.MongoDBConnection`\n- All values in `engine` (except `class`) are passed to the `__init__` method of the specified class.\n- The `query` key is optional for the default supported engines, but you can customize it if needed.\n\n---\n\n## PantherDB\n\nExample configuration for PantherDB:\n\n```python\nDATABASE = {\n    'engine': {\n        'class': 'panther.db.connections.PantherDBConnection',\n        'path': BASE_DIR / 'database.pdb',  # Optional\n        'encryption': True  # Optional, default is False\n    }\n}\n```\n\n### Notes\n- `path` is optional; you can customize the directory and filename of your database.\n- `encryption` is optional and defaults to `False`.\n- The `cryptography` package is required if you set `encryption` to `True`.\n\n---\n\n## MongoDB\n\nExample configuration for MongoDB:\n\n```python\nDATABASE = {\n    'engine': {\n        'class': 'panther.db.connections.MongoDBConnection',\n        'host': 'mongodb://127.0.0.1:27017/database_name'\n    }\n}\n```\n\n### Notes\n- The parameters for the engine are the same as those for `pymongo.MongoClient`. See the [PyMongo documentation](https://pymongo.readthedocs.io/en/stable/tutorial.html#making-a-connection-with-mongoclient) for details.\n\n---\n\n## How Does It Work?\n\n- Panther creates a database connection based on the `DATABASE` configuration you define in your configs.\n- You can access this connection through your models, or directly via:\n  ```python\n  from panther.db.connections import db\n  ```\n"
  },
  {
    "path": "docs/docs/deployment.md",
    "content": "# Deploying Panther Applications\n\nPanther is an ASGI-compatible web framework, which means you can deploy your Panther app using any ASGI server, such as **Uvicorn**, **Granian**, **Daphne**, or others. This guide covers best practices and options for deploying your Panther application in production.\n\n---\n\n## 1. Production-Ready ASGI Servers\n\nWhile Panther comes with a convenient CLI, you are not limited to it. You can use any ASGI server to run your app:\n\n- **Uvicorn** (default, recommended)\n- **Granian**\n- **Daphne**\n- **Hypercorn**\n- Any other ASGI-compliant server\n\n### Example: Using Uvicorn Directly\n\n```bash\nuvicorn main:app --host 0.0.0.0 --port 8000 --workers 4\n```\n\n- `main:app` refers to your application instance (e.g., `app = Panther(...)` in `main.py`).\n- Adjust `--workers` for your server's CPU count.\n\n### Example: Using Gunicorn with Uvicorn Workers\n\nFor robust process management and multiple workers:\n\n```bash\ngunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app\n```\n\n---\n\n## 2. Using the Panther CLI\n\nPanther provides a built-in CLI for development and deployment. The command:\n\n```bash\npanther run main:app --reload\n```\n\nis **an alias for running Uvicorn** with your Panther app. You can use all Uvicorn options with this command.\n\n- `--reload` is for development (auto-reloads on code changes).\n- Omit `--reload` for production.\n\n**Note:** For advanced deployment, prefer running Uvicorn (or another ASGI server) directly, as shown above.\n\n---\n\n## 3. Environment Variables & Configuration\n\n- Set environment variables (e.g., `PORT`, `HOST`, `DATABASE_URL`) as needed for your deployment environment.\n- Use a process manager (e.g., **systemd**, **supervisor**, **pm2**, or **Docker** (recomended)) to keep your app running and restart on failure.\n\n---\n\n## 4. Static Files & Reverse Proxy\n\nPanther does not serve static files in production. Use a reverse proxy (like **Nginx** or **Caddy**) to:\n\n- Serve static files (JS, CSS, images)\n- Forward requests to your ASGI server (Uvicorn, etc.)\n- Handle HTTPS/SSL termination\n\n**Example Nginx config:**\n```nginx\nlocation / {\n    proxy_pass http://127.0.0.1:8000;\n    proxy_set_header Host $host;\n    proxy_set_header X-Real-IP $remote_addr;\n}\n```\n\n---\n\n## 5. WebSocket & Multiple Workers\n\nIf your app uses WebSockets and you want to run multiple workers, configure **Redis** for pub/sub support (see [Redis Integration](redis.md)).  \nAlternatively, use the `--preload` flag with Gunicorn for basic multi-worker support (see [WebSocket docs](websocket.md)).\n\n---\n\n## 6. Example: Docker Deployment\n\nPanther can be efficiently containerized using a multi-stage Docker build and the [uv](https://github.com/astral-sh/uv) package manager, which is significantly faster and more reliable than pip. This approach results in smaller, more secure images and faster build times.\n\n### Advantages of Multi-Stage & uv\n- **Smaller final image size**: Only production dependencies and app code are included.\n- **Faster dependency installation**: `uv` is much faster than `pip` and supports modern lockfiles.\n- **Better security**: Build tools and caches are left behind in the builder stage.\n- **Cleaner builds**: No unnecessary files in the final image.\n\n### Example Multi-Stage Dockerfile with uv\n\n```dockerfile\n# --- Builder ---\nFROM python:3.12 AS builder\nWORKDIR /app\n\n# Create virtual environment\nRUN python -m venv /opt/venv\n\n# Install uv into system (can be reused)\nRUN pip install --no-cache-dir uv\n\n# Copy requirements and install into the venv\nCOPY requirements.txt .\nRUN /usr/local/bin/uv pip install -r requirements.txt --python /opt/venv/bin/python\n\n# --- Final image ---\nFROM python:3.12-slim AS production\n\nENV PYTHONUNBUFFERED=1\nENV PATH=\"/opt/venv/bin:$PATH\"\n\n# Copy venv from builder\nCOPY --from=builder /opt/venv /opt/venv\n\n# Copy app source\nWORKDIR /app\nCOPY . /app\n\n# Run the app\nCMD [\"uvicorn\", \"main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]\n```\n"
  },
  {
    "path": "docs/docs/events.md",
    "content": "# Application Events\n\nPanther provides a simple way to execute custom logic during your application's lifecycle using event decorators. You can hook into the **startup** and **shutdown** phases to run code when your app starts or stops.\n\n---\n\n## Startup Event\n\nTo run code when your application starts, use the `@Event.startup` decorator:\n\n```python title=\"events.py\" linenums=\"1\"\nfrom panther.events import Event\n\n@Event.startup\ndef do_something_on_startup():\n    print('Hello, I am at startup')\n```\n\nYou can define multiple startup event handlers. They will be executed in the order they are registered.\n\n---\n\n## Shutdown Event\n\nTo run code when your application is shutting down, use the `@Event.shutdown` decorator:\n\n```python title=\"events.py\" linenums=\"1\"\nfrom panther.events import Event\n\n@Event.shutdown\ndef do_something_on_shutdown():\n    print('Good Bye, I am at shutdown')\n```\n\nYou can define multiple shutdown event handlers as well.\n\n---\n\n## Additional Notes\n\n- **Multiple Handlers:** You can register multiple functions for both `startup` and `shutdown` events. All will be called.\n- **Sync & Async Support:** Event handlers can be either synchronous or asynchronous functions.\n- **Use Cases:** Common use cases include initializing resources (like database connections) on startup and cleaning up resources on shutdown.\n"
  },
  {
    "path": "docs/docs/file_handling.md",
    "content": "# File Handling in Panther\n\nPanther provides file handling capabilities through the `File` and `Image` classes for file uploads and processing.\n\n---\n\n## Overview\n\n- **`File`**: General-purpose file handling for any type of file\n- **`Image`**: Specialized file handling for images with automatic MIME type validation\n\n---\n\n## Basic Usage\n\n### Importing File Classes\n\n```python\nfrom panther.file_handler import File, Image\n```\n\n### Creating File Objects\n\n```python\n# Create from bytes (in-memory file)\nfile = File(\n    file_name='document.pdf',\n    content_type='application/pdf',\n    file=b'PDF content here...'\n)\n\n# Create image file\nimage = Image(\n    file_name='photo.jpg',\n    content_type='image/jpeg',\n    file=b'JPEG content here...'\n)\n```\n\n---\n\n## File Properties\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `file_name` | str | The name of the file |\n| `content_type` | str | The MIME type of the file |\n| `file` | bytes \\| None | The file content in bytes |\n| `size` | int | File size in bytes |\n\n---\n\n## File Methods\n\n```python\n# Read file content\ncontent = file.read()\n\n# Save file to disk\npath = file.save(\"uploads/\")\n\n# Use as context manager\nwith file as f:\n    content = f.read()\n```\n\n---\n\n## Saving Files\n\nThe `save()` method provides flexible file saving with several features:\n\n### Basic Saving\n\n```python\n# Save with original filename\npath = file.save()\n\n# Save with custom filename\npath = file.save(\"custom_name.txt\")\n\n# Save with overwrite\npath = file.save(\"existing_file.txt\", overwrite=True)\n```\n\n### Directory Path Handling\n\nWhen you provide a path ending with `/`, it's treated as a directory:\n\n```python\n# Save to directory with original filename\npath = file.save(\"uploads/\")  # Saves as \"uploads/original_filename.ext\"\n\n# Save to nested directory\npath = file.save(\"uploads/images/\")  # Saves as \"uploads/images/original_filename.ext\"\n\n# Save to directory with custom filename\npath = file.save(\"uploads/custom_name.txt\")  # Saves as \"uploads/custom_name.txt\"\n```\n\n### Idempotent Behavior\n\nThe `save()` method is **idempotent** - calling it multiple times on the same file instance returns the same path:\n\n```python\nfile = File(file_name='test.txt', content_type='text/plain', file=b'content')\n\n# First call - creates file and returns path\npath1 = file.save(\"uploads/\")\nprint(path1)  # \"uploads/test.txt\"\n\n# Subsequent calls - returns same path without creating new files\npath2 = file.save(\"uploads/\")\npath3 = file.save(\"uploads/\")\n\nassert path1 == path2 == path3  # All return the same path\n```\n\nThis prevents accidental creation of duplicate files with different timestamps.\n\n### Automatic Directory Creation\n\nThe `save()` method automatically creates directories if they don't exist:\n\n```python\n# This will create the \"uploads/images\" directory structure if it doesn't exist\npath = file.save(\"uploads/images/\")\n```\n\n---\n\n## Integration with Models\n\nFiles can be used as model attributes:\n\n```python\nfrom datetime import datetime\nfrom panther.db import Model\nfrom panther.file_handler import File, Image\n\nclass Document(Model):\n    title: str\n    file: File\n    uploaded_at: datetime\n\nclass Profile(Model):\n    name: str\n    avatar: Image\n    bio: str | None = None\n```\n\nWhen using File types in models:\n- The file metadata (name, content type) is preserved\n- The file path is stored in the database\n- The actual file content is automatically saved to disk when inserting the model\n- You can access file properties and methods on the model instance\n\n---\n\n## Integration with Serializers\n\n```python\nfrom panther.serializer import ModelSerializer\nfrom panther.file_handler import File, Image\n\nclass FileUploadSerializer(ModelSerializer):\n    class Config:\n        model = FileUpload\n        fields = ['file', 'description']\n        required_fields = ['file']\n\nclass ImageUploadSerializer(ModelSerializer):\n    class Config:\n        model = ImageUpload\n        fields = ['image', 'title']\n        required_fields = ['image']\n```\n\n### File Validation\n\n```python\nfrom pydantic import field_validator\nfrom panther import status\nfrom panther.exceptions import APIError\nfrom panther.file_handler import File\nfrom panther.serializer import ModelSerializer\n\nclass DocumentUploadSerializer(ModelSerializer):\n    class Config:\n        model = DocumentUpload\n        fields = ['file', 'title']\n        required_fields = ['file']\n\n    @field_validator('file')\n    @classmethod\n    def validate_file_size(cls, file: File) -> File:\n        if file.size > 10 * 1024 * 1024:  # 10MB limit\n            raise APIError(\n                detail='File size must be less than 10MB',\n                status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE\n            )\n        return file\n```\n\n---\n\n## API Integration\n\n### File Upload API\n\n```python\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\n\n@API(input_model=FileUploadSerializer)\nasync def upload_file(request: Request):\n    file_data = request.validated_data\n    file = file_data.file\n    \n    # Save file to disk\n    saved_path = file.save(\"uploads/\")\n    \n    # Store in database\n    document = await Document.insert_one({\n        'title': file_data.description,\n        'file': file,\n        'uploaded_at': datetime.now()\n    })\n    \n    return Response(data={\n        \"message\": \"File uploaded successfully\",\n        \"saved_path\": saved_path\n    })\n```\n\n---\n\n## Error Handling\n\n```python\nfrom panther import status\nfrom panther.exceptions import APIError\n\n@API(input_model=FileUploadSerializer)\nasync def upload_file(request: Request):\n    try:\n        file_data = request.validated_data\n        file = file_data.file\n        \n        # Check file size\n        if file.size > 10 * 1024 * 1024:  # 10MB limit\n            raise APIError(\n                detail=\"File too large. Maximum size is 10MB.\",\n                status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE\n            )\n        \n        saved_path = file.save(\"uploads/\")\n        return Response(data={\"saved_path\": saved_path})\n        \n    except Exception as e:\n        raise APIError(\n            detail=f\"File upload failed: {str(e)}\",\n            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR\n        )\n```\n\n---\n\n## Best Practices\n\n1. **Validate file types**: Always check MIME types to prevent malicious uploads\n2. **Limit file sizes**: Set reasonable size limits to prevent abuse\n3. **Use context managers**: When reading files, use `with` statements for proper cleanup\n4. **Handle errors gracefully**: Provide clear error messages for validation failures\n5. **Secure file storage**: Always validate and sanitize file names before saving\n6. **Leverage idempotency**: The `save()` method is idempotent, so you can call it multiple times safely\n7. **Use directory paths**: When saving files, use directory paths ending with `/` for better organization\n\nThis guide covers the essential aspects of file handling in Panther. For more advanced features and detailed examples, refer to the API documentation and examples. "
  },
  {
    "path": "docs/docs/first_crud.md",
    "content": "We assume you have successfully set up your project following the [Introduction](index.md#installation) guide.\n\nIn this guide, we will create a `CRUD` (`Create`, `Retrieve`, `Update`, and `Delete`) API for managing `Book` entities.\n\n---\n\n## Project Structure\nThe final structure of your project will be as follows:\n```\n.\n├── app\n│     ├── apis.py\n│     ├── models.py\n│     ├── serializers.py\n│     └── urls.py\n└── core\n      ├── configs.py\n      └── urls.py\n```\n\n## Configuring the Database\n\n!!! question \"How does the database work in Panther?\"\n    Refer to [Database](database.md) to learn about supported databases and their functionality.\n\nConfigure the `DATABASE` settings in `core/configs.py`. In this guide, we will use `PantherDB`.\n\n> [PantherDB](https://github.com/PantherPy/PantherDB/#readme) is a simple, file-based, document-oriented database.\n\n```python title=\"core/configs.py\"\nDATABASE = {\n    'engine': {\n        'class': 'panther.db.connections.PantherDBConnection',\n    }\n}\n```\n\n---\n\n## Defining the Model\n\n!!! question \"How do models work in Panther?\"\n    Refer to [Models](model.md) to learn more about defining models and how they function.\n\n!!! tip \"File Handling in Models\"\n    For models that need to handle file uploads, see [File Handling](file_handling.md) for detailed information about using `File` and `Image` types in your models.\n\nCreate a model named `Book` in `app/models.py`:\n\n```python title=\"app/models.py\" linenums=\"1\"\nfrom panther.db import Model\n\n\nclass Book(Model):\n    name: str\n    author: str\n    pages_count: int\n```  \n\n---\n\n## Defining URLs\n\n!!! question \"How do URLs work in Panther?\"\n    Refer to [URLs](urls.md) to learn more about URL definitions.\n\nThe base `urls` configuration should include all application URLs.\n\n```python title=\"core/urls.py\" linenums=\"1\"\nfrom app.urls import urls as app_urls\n\n\nurls = {\n    '/': app_urls,\n}\n```  \nIn `app/urls.py`, define the `Book` API URLs:\n\n=== \"Function-Base\"\n\n    ```python title=\"app/urls.py\" linenums=\"1\"\n    from app.apis import book_api, single_book_api\n    \n    urls = {\n        'book/': book_api,\n        'book/<book_id>/': single_book_api,\n    }\n    ```\n\n=== \"Class-Base API/ Generic API (Beta)\"\n\n    ```python title=\"app/urls.py\" linenums=\"1\"\n    from app.apis import BookAPI, SingleBookAPI\n    \n    urls = {\n        'book/': BookAPI,\n        'book/<book_id>/': SingleBookAPI,\n    }\n    ```\n\n---\n\n## Defining the Serializer\n\n!!! question \"How do serializers work in Panther?\"\n    Refer to [Serializer](serializer.md) to learn more about available serializers.\n\n!!! tip \"File Handling in Serializers\"\n    For serializers that need to handle file uploads, see [File Handling](file_handling.md) for information about validating and processing files in your serializers.\n\nSerializers transform data between the application and API requests.\n\n=== \"Function-Base/ Class-Base API\"\n    The serializer can be inherited from `ModelSerializer` or `pydantic.BaseModel`\n\n    ```python title=\"app/serializers.py\" linenums=\"1\"\n    from panther.serializer import ModelSerializer\n    \n    from app.models import Book\n    \n    class BookSerializer(ModelSerializer):\n        class Config:\n            model = Book\n            fields = ['name', 'author', 'pages_count']\n    ```\n    or\n\n    ```python title=\"app/serializers.py\" linenums=\"1\"\n    import pydantic\n    \n    from app.models import Book\n    \n    class BookSerializer(pydantic.BaseModel):\n        name: str\n        author: str\n        pages_count: int\n    ```\n\n=== \"Generic API (Beta)\"\n    The serializer should be inherited from `ModelSerializer` to be used in `GenericAPI`.\n\n    ```python title=\"app/serializers.py\" linenums=\"1\"\n    from panther.serializer import ModelSerializer\n    \n    from app.models import Book\n    \n    class BookSerializer(ModelSerializer):\n        class Config:\n            model = Book\n            fields = ['name', 'author', 'pages_count']\n    ```\n\n\n### APIs\n!!! question \"How do APIs work in Panther?\"\n    Refer to [API](api.md) to learn more about API types and their usage.\n\n!!! tip \"File Upload APIs\"\n    For APIs that handle file uploads, see [File Handling](file_handling.md) for comprehensive examples of file upload endpoints and processing.\n\n#### Create\n\n=== \"Function-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther import status\n    from panther.app import API\n    from panther.request import Request\n    from panther.response import Response\n\n    from app.serializers import BookSerializer\n    from app.models import Book\n    \n    \n    @API(input_model=BookSerializer, methods=['POST'])\n    async def book_api(request: Request):\n        body: BookSerializer = request.validated_data\n        book: Book = await Book.insert_one(\n            name=body.name,\n            author=body.author,\n            pages_count=body.pages_count,\n        )\n        return Response(data=book, status_code=status.HTTP_201_CREATED)\n    ```\n\n=== \"Class-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther import status\n    from panther.app import GenericAPI\n    from panther.request import Request\n    from panther.response import Response\n    \n    from app.serializers import BookSerializer\n    from app.models import Book\n    \n    \n    class BookAPI(GenericAPI):\n        input_model = BookSerializer\n    \n        async def post(self, request: Request):\n            body: BookSerializer = request.validated_data\n            book = await Book.insert_one(\n                name=body.name,\n                author=body.author,\n                pages_count=body.pages_count,\n            )\n            return Response(data=book, status_code=status.HTTP_201_CREATED)\n    ```\n\n=== \"Generic API (Beta)\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther.app import CreateAPI\n\n    from app.serializers import BookSerializer\n    \n    \n    class BookAPI(CreateAPI):\n        input_model = BookSerializer\n    ```\n\n#### List\n\n=== \"Function-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther import status\n    from panther.app import API\n    from panther.request import Request\n    from panther.response import Response\n\n    from app.serializers import BookSerializer\n    from app.models import Book\n    \n    \n    @API(input_model=BookSerializer, methods=['POST', 'GET'])\n    async def book_api(request: Request):\n        ...\n        if request.method == 'GET':\n            books = await Book.find()\n            return Response(data=books, status_code=status.HTTP_200_OK)\n    ```\n\n=== \"Class-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther import status\n    from panther.app import GenericAPI\n    from panther.response import Response\n    \n    from app.serializers import BookSerializer\n    from app.models import Book\n    \n    \n    class BookAPI(GenericAPI):\n        input_model = BookSerializer\n\n        async def get(self):\n            books = await Book.find()\n            return Response(data=books, status_code=status.HTTP_200_OK)\n    ```\n\n=== \"Generic API (Beta)\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther.generics import CreateAPI, ListAPI\n    from panther.pagination import Pagination\n    from panther.request import Request\n    \n    from app.models import Book\n    from app.serializers import BookSerializer\n        \n    class BookAPI(CreateAPI, ListAPI):\n        input_model = BookSerializer\n        pagination = Pagination  #(1)!\n        search_fields = ['name', 'author']  #(2)!\n        filter_fields = ['name', 'author']  #(3)!\n        sort_fields = ['name', 'pages_count']  #(4)!\n    \n        async def get_query(self, request: Request, **kwargs):\n            return await Book.find()\n    ```\n\n    1. `Pagination` class will look for the `limit` and `skip` in the `query params` and paginate your response and then return it on its own response template.\n    2. The query will be changed and looking for the value of the `search` query param in these fields,\n    e.g. query param is the `?search=TheLittlePrince`, we will looking for the Book with `name` or `author` of `TheLittlePrince`.\n    3. It will look for each value of the `filter_fields` in the `query params` and query on them, \n    e.g. `?name=Something&author=Ali`, it will looks for Book that its `author` is `Ali` and its `name` is `Something`.\n    4. The query will be sortalbe with the fields which is in `sort_fields`.\n\n#### Retrieve\n\n=== \"Function-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther import status\n    from panther.app import API\n    from panther.request import Request\n    from panther.response import Response\n\n    from app.models import Book\n    \n   \n    @API(methods=['GET'])\n    async def single_book_api(request: Request, book_id: int):\n        if book := await Book.find_one(id=book_id):\n            return Response(data=book, status_code=status.HTTP_200_OK)\n        return Response(data={'detail': 'Book not found'}, status_code=status.HTTP_400_BAD_REQUEST)\n    ```\n\n=== \"Class-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther import status\n    from panther.app import GenericAPI\n    from panther.response import Response\n    \n    from app.models import Book\n    \n   \n    class SingleBookAPI(GenericAPI):\n        async def get(self, book_id: int):\n            if book := await Book.find_one(id=book_id):\n                return Response(data=book, status_code=status.HTTP_200_OK)\n            return Response(data={'detail': 'Book not found'}, status_code=status.HTTP_400_BAD_REQUEST)\n    ```\n\n=== \"Generic API (Beta)\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther.generics import RetrieveAPI\n    from panther.request import Request\n\n    from app.models import Book\n    \n    \n    class SingleBookAPI(RetrieveAPI):\n        async def get_instance(self, request: Request, **kwargs):\n            return await Book.find_one_or_raise(id=kwargs['book_id'])\n    ```\n\n#### Update\n\n=== \"Function-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther import status\n    from panther.app import API\n    from panther.request import Request\n    from panther.response import Response\n\n    from app.models import Book\n    from app.serializers import BookSerializer \n    \n   \n    @API(input_model=BookSerializer, methods=['GET', 'PUT'])\n    async def single_book_api(request: Request, book_id: int):\n        ...\n        if request.method == 'PUT':            \n            is_updated = await Book.update_one({'id': book_id}, request.validated_data.model_dump())\n            data = {'is_updated': is_updated}\n            return Response(data=data, status_code=status.HTTP_200_OK)\n    ```\n\n=== \"Class-Base API\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther import status\n    from panther.app import GenericAPI\n    from panther.response import Response\n    \n    from app.models import Book\n    \n   \n    class SingleBookAPI(GenericAPI):\n        input_model = BookSerializer\n\n        async def put(self, request: Request, book_id: int):\n            is_updated = await Book.update_one({'id': book_id}, request.validated_data.model_dump())\n            data = {'is_updated': is_updated}\n            return Response(data=data, status_code=status.HTTP_200_OK)\n    ```\n\n=== \"Generic API (Beta)\"\n\n    ```python title=\"app/apis.py\" linenums=\"1\"\n    from panther.generics import RetrieveAPI\n    from panther.request import Request\n\n    from app.models import Book\n    from app.serializers import BookSerializer\n    \n    \n    class SingleBookAPI(RetrieveAPI, UpdateAPI):\n        input_model = BookSerializer\n\n        async def get_instance(self, request: Request, **kwargs):\n            return await Book.find_one_or_raise(id=kwargs['book_id'])\n    ```\n\n\n#### Delete\n\n=== \"Function-Base API\"\n\n    ```python title=\"apis.py\" linenums=\"1\"\n    from panther import status\n    from panther.app import API\n    from panther.request import Request\n    from panther.response import Response\n\n    from app.models import Book\n    from app.serializers import BookSerializer \n    \n   \n    @API(input_model=BookSerializer, methods=['GET', 'PUT', 'DELETE'])\n    async def single_book_api(request: Request, book_id: int):\n        ...\n        if request.method == 'DELETE':\n            await Book.delete_one(id=book_id)\n            return Response(status_code=status.HTTP_204_NO_CONTENT)\n    ```\n\n=== \"Class-Base API\"\n\n    ```python title=\"apis.py\" linenums=\"1\"\n    from panther import status\n    from panther.app import GenericAPI\n    from panther.response import Response\n    \n    from app.models import Book\n    \n   \n    class SingleBookAPI(GenericAPI):\n        async def delete(self, book_id: int):\n            await Book.delete_one(id=book_id)\n            return Response(status_code=status.HTTP_204_NO_CONTENT)\n    ```\n\n=== \"Generic API (Beta)\"\n\n    ```python title=\"apis.py\" linenums=\"1\"\n    from panther.generics import RetrieveAPI\n    from panther.request import Request\n\n    from app.models import Book\n    from app.serializers import BookSerializer\n    \n    \n    class SingleBookAPI(RetrieveAPI, UpdateAPI, DeleteAPI):\n        input_model = BookSerializer\n\n        async def get_instance(self, request: Request, **kwargs):\n            return await Book.find_one_or_raise(id=kwargs['book_id'])\n    ```\n\nWith this, you now have a complete CRUD API implementation for the `Book` entity.\n"
  },
  {
    "path": "docs/docs/index.md",
    "content": "## Panther\n**A Fast & Friendly Web Framework for Building Async APIs with Python 3.10+**\n\n<p align=\"center\">\n  <img src=\"https://github.com/AliRn76/panther/raw/master/docs/docs/images/logo-vertical.png\" alt=\"Panther Logo\" style=\"width: 450px\">\n</p>\n\n[![PyPI](https://img.shields.io/pypi/v/panther?label=PyPI)](https://pypi.org/project/panther/)  [![PyVersion](https://img.shields.io/pypi/pyversions/panther.svg)](https://pypi.org/project/panther/) [![codecov](https://codecov.io/github/AliRn76/panther/graph/badge.svg?token=YWFQA43GSP)](https://codecov.io/github/AliRn76/panther) [![Downloads](https://static.pepy.tech/badge/panther/month)](https://pepy.tech/project/panther) [![License](https://img.shields.io/github/license/alirn76/panther.svg)](https://github.com/alirn76/panther/blob/main/LICENSE)\n\n---\n\n## 🐾 Why Choose Panther?\n\nPanther is designed to be **fast**, **simple**, and **powerful**. Here's what makes it special:\n\n- **One of the fastest Python frameworks** available\n- **File-based database** ([PantherDB](https://pypi.org/project/pantherdb/)) - No external database setup required\n- **Document-oriented ODM** - Supports MongoDB & PantherDB with familiar syntax\n- **API caching system** - In-memory and Redis support\n- **OpenAPI/Swagger** - Auto-generated API documentation\n- **WebSocket support** - Real-time communication out of the box\n- **Authentication & Permissions** - Built-in security features\n- **Background tasks** - Handle long-running operations\n- **Middleware & Throttling** - Extensible and configurable\n- **File upload handling** - Built-in File and Image classes for easy file processing\n\n---\n\n## Benchmark\n\n<p align=\"center\">\n  <img src=\"https://github.com/AliRn76/panther/raw/master/docs/docs/images/benchmark.png\" alt=\"Benchmark\" style=\"width: 800px\">\n</p>\n\n[[TechEmpower Benchmark]](https://www.techempower.com/benchmarks/#section=data-r23&l=zijzen-pa7&c=4)\n\n---\n\n## Supported by\n\n<center>\n<a href=\"https://drive.google.com/file/d/17xe1hicIiRF7SQ-clg9SETdc19SktCbV/view?usp=sharing\">\n<img alt=\"JetBrains\" src=\"https://github.com/AliRn76/panther/raw/master/docs/docs/images/jb_beam_50x50.png\">\n</a>\n</center>\n\n---\n\n## Installation\n\n   ```shell\n   $ pip install panther\n   ```\n\n---\n\n## Getting Started\n\n### Quick Start Guide\n\n1. **Create a new project directory**\n   ```shell\n   $ mkdir my_panther_app\n   $ cd my_panther_app\n   ```\n\n2. **Set up your environment**\n   ```shell\n   $ python3 -m venv .venv\n   $ source .venv/bin/activate  # On Windows: .\\.venv\\Scripts\\activate\n   $ pip install panther\n   ```\n\n3. **Create your first application**\n    \n    Create a `main.py` file with one of the examples below.\n\n### Basic API Example\n\nHere's a simple REST API endpoint that returns a \"Hello World\" message:\n\n```python title=\"main.py\" linenums=\"1\"\nfrom datetime import datetime, timedelta\nfrom panther import status, Panther\nfrom panther.app import GenericAPI\nfrom panther.openapi.views import ScalarOpenAPI\nfrom panther.response import Response\n\nclass HelloAPI(GenericAPI):\n    # Cache responses for 10 seconds\n    cache = timedelta(seconds=10)\n    \n    def get(self):\n        current_time = datetime.now().isoformat()\n        return Response(\n            data={'message': f'Hello from Panther! 🐾 | {current_time}'},\n            status_code=status.HTTP_200_OK\n        )\n\n# URL routing configuration\nurl_routing = {\n    '/': HelloAPI,\n    'docs/': ScalarOpenAPI,  # Auto-generated API docs\n}\n\n# Create your Panther app\napp = Panther(__name__, configs=__name__, urls=url_routing)\n```\n\n### WebSocket Example\n\nHere's a simple WebSocket echo server that sends back any message it receives:\n\n```python title=\"main.py\" linenums=\"1\"\nfrom panther import Panther\nfrom panther.app import GenericAPI\nfrom panther.response import HTMLResponse\nfrom panther.websocket import GenericWebsocket\n\nclass EchoWebsocket(GenericWebsocket):\n    async def connect(self, **kwargs):\n        await self.accept()\n        await self.send(\"Connected to Panther WebSocket!\")\n    \n    async def receive(self, data: str | bytes):\n        # Echo back the received message\n        await self.send(f\"Echo: {data}\")\n\nclass WebSocketPage(GenericAPI):\n    def get(self):\n        template = \"\"\"\n        <h2>🐾 Panther WebSocket Echo Server</h2>\n        <input id=\"msg\"><button onclick=\"s.send(msg.value)\">Send</button>\n        <ul id=\"log\"></ul>\n        <script>\n            const s = new WebSocket('ws://127.0.0.1:8000/ws');\n            s.onmessage = e => log.innerHTML += `<li><- ${msg.value}</li><li>-> ${e.data}</li>`;\n        </script>\n        \"\"\"\n        return HTMLResponse(template)\n\nurl_routing = {\n    '': WebSocketPage,\n    'ws': EchoWebsocket,\n}\napp = Panther(__name__, configs=__name__, urls=url_routing)\n```\n\n### Running Your Application\n\n1. **Start the development server**\n   ```shell\n   $ panther run main:app --reload\n   ```\n   \n    > **Note:** Panther uses [Uvicorn](https://github.com/encode/uvicorn) as the default ASGI server, but you can also use [Granian](https://pypi.org/project/granian/), [Daphne](https://pypi.org/project/daphne/), or any ASGI-compatible server.\n\n2. **Test your application**\n    - For the _API_ example: Visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/) to see the \"Hello World\" response\n    - For the _WebSocket_ example: Visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/) and send a message. \n"
  },
  {
    "path": "docs/docs/log_queries.md",
    "content": "# Query Performance Logging\n\nPanther provides built-in query performance monitoring that logs the execution time of each database query when enabled. \n\n## Configuration\n\nTo enable query performance logging, set the `LOG_QUERIES` configuration option to `True`:\n\n```python\nLOG_QUERIES = True  # Default is False\n```\n\n## How It Works\n\nWhen `LOG_QUERIES` is enabled, Panther automatically:\n\n- **Measures execution time** for every database query\n- **Logs performance data** to `logs/query.log`\n- **Includes query details** such as method name and execution time in milliseconds\n\n## Log Format\n\nEach log entry follows this format:\n\n```\nINFO: | [Timestamp] | [Query] [Method] takes [Duration] ms\n```\n\n### Example Log Entries\n\n```python\nINFO: | 2023-03-19 20:37:27 | [Query] User.insert_one() takes 1.612 ms\nINFO: | 2023-03-19 20:37:28 | [Query] User.find() takes 45.234 ms\nINFO: | 2023-03-19 20:37:29 | [Query] Post.update_one() takes 12.891 ms\nINFO: | 2023-03-19 20:37:30 | [Query] Comment.delete_one() takes 3.445 ms\n```\n\n## Use Cases\n\nQuery logging is particularly useful for:\n\n- **Development**: Identifying slow queries during development\n- **Debugging**: Troubleshooting performance issues\n- **Optimization**: Finding bottlenecks in database operations\n- **Monitoring**: Tracking query performance over time\n\n## Performance Considerations\n\nKeep `LOG_QUERIES = False` in production environments for optimal performance.\n\nQuery logging adds a small overhead to each database operation, which can impact application performance under high load.\n"
  },
  {
    "path": "docs/docs/middlewares.md",
    "content": "# Middlewares in Panther\n\nMiddlewares allow you to process requests and responses globally or for all or specific APIs. They are useful for tasks such as logging, timing, and other cross-cutting concerns.\n\n---\n\n## Global Middlewares\n\nTo apply middlewares globally, define a `MIDDLEWARES` list in your configs for HTTP middlewares, and a `WS_MIDDLEWARE` list for WebSocket middlewares. **Only HTTP-related middlewares should be placed in `MIDDLEWARES`, and WebSocket middlewares must be placed in `WS_MIDDLEWARE`.**\n\n> **Note:** Previously, WebSocket middlewares were also defined in `MIDDLEWARES`. Now, you must define them in `WS_MIDDLEWARE` instead.\n\nEach item can be either a string (dotted path to the middleware class) or the class itself (useful for single-file applications):\n\n```python\nclass Middleware(HTTPMiddleware):\n    pass\n\nMIDDLEWARES = [\n    'core.middlewares.MyMiddleware',  # Import by dotted path\n    Middleware,                       # Or directly by class\n]\n\nWS_MIDDLEWARE = [\n    'core.middlewares.MyWebsocketMiddleware',  # WebSocket middleware by dotted path\n    MyWebsocketMiddleware,                     # Or directly by class\n]\n```\n\n---\n\n## Per-API Middlewares\n\nYou can assign middlewares to specific APIs, either class-based or function-based:\n\n```python title=\"apis.py\" linenums=\"1\"\nfrom panther.app import API, GenericAPI\nfrom panther.middlewares import HTTPMiddleware\n\nclass Middleware(HTTPMiddleware):\n    pass\n\n# Class-Based API\nclass MyAPI(GenericAPI):\n    middlewares = [Middleware]\n\n# Function-Based API\n@API(middlewares=[Middleware])\ndef my_api():\n    pass\n```\n\n---\n\n## Middleware Execution Order\n\nMiddlewares are executed in the following order:\n\n1. Global middlewares: `__call__()` (enter)\n2. Per-API middlewares: `__call__()` (enter)\n3. Per-API middlewares: `dispatch` (return)\n4. Global middlewares: `dispatch` (return)\n\n---\n\n## Creating Custom Middlewares\n\n### Types of Middlewares\n\nPanther provides two types of middleware base classes:\n\n- `HTTPMiddleware`: For HTTP requests only\n- `WebsocketMiddleware`: For WebSocket connections only\n\nMake sure to inherit from the correct base class:\n\n```python\n# For HTTP requests\nfrom panther.middlewares import HTTPMiddleware\n\n# For WebSocket connections\nfrom panther.middlewares import WebsocketMiddleware\n```\n\n### Implementing a Middleware\n\n- Create a class inheriting from `HTTPMiddleware` or `WebsocketMiddleware`.\n- Implement an asynchronous `__call__` method.\n- Always return either `await self.dispatch(...)` or a `Response`/`GenericWebsocket` instance at the end of `__call__()`.\n\n#### Example: HTTP Middleware\n\n```python title=\"middlewares.py\" linenums=\"1\"\nfrom datetime import datetime\nfrom panther.middlewares.base import HTTPMiddleware\nfrom panther.request import Request\nfrom panther.response import Response\n\nclass CustomMiddleware(HTTPMiddleware):\n    async def __call__(self, request: Request) -> Response:\n        start_time = datetime.now()\n        response = await self.dispatch(request=request)\n        duration = datetime.now() - start_time\n        print(f'Request took {duration.total_seconds()} seconds')\n        return response\n```\n\n#### Example: WebSocket Middleware\n\n```python title=\"middlewares.py\" linenums=\"1\"\nfrom datetime import datetime\nfrom panther.middlewares.base import WebsocketMiddleware\nfrom panther.websocket import GenericWebsocket, Websocket\n\nclass TimerMiddleware(WebsocketMiddleware):\n    async def __call__(self, connection: Websocket) -> GenericWebsocket:\n        start_time = datetime.now()\n        response = await self.dispatch(connection=connection)\n        duration = datetime.now() - start_time\n        print(f'Connection lasted {duration.total_seconds()} seconds')\n        return response\n```\n\n---\n\n## Built-in Middlewares\n\nPanther provides several built-in middlewares to help with common tasks. Below are the available options and how to use them:\n\n### CORS Middleware\n- **Purpose:** Enables Cross-Origin Resource Sharing (CORS) for your APIs.\n- **Usage:** Add `panther.middlewares.CORSMiddleware` to your global `MIDDLEWARES` list.\n- **Configuration:** Requires specific global settings. See the [CORS Middleware documentation](cors.md) for configuration details.\n\n### Monitoring Middleware\n- **Purpose:** Logs request and connection data for monitoring and analytics.\n- **Usage:** Add `panther.middlewares.MonitoringMiddleware` to your global `MIDDLEWARES` list.\n- **Note:** This middleware or `WebsocketMonitoringMiddleware` is required if you want to use the `panther monitor` command.\n\n### WebSocket Monitoring Middleware\n- **Purpose:** Similar to `MonitoringMiddleware`, but specifically logs data for WebSocket connections.\n- **Usage:** Add `panther.middlewares.WebsocketMonitoringMiddleware` to your global `MIDDLEWARES` list if you want to monitor WebSocket traffic.\n- **Note:** This middleware or `MonitoringMiddleware` is required if you want to use the `panther monitor` command.\n\n---\n\n## Tips\n\n- Use global middlewares for logic that should apply to all requests.\n- Use per-API middlewares for logic specific to certain endpoints.\n- Always ensure your `__call__` method is asynchronous and returns the appropriate value.\n"
  },
  {
    "path": "docs/docs/model.md",
    "content": "# Panther Models\n\nPanther models allow you to define your data schema and interact with the database using Python classes. They are built on top of `pydantic.BaseModel` and extend its functionality with database query capabilities.\n\n## Creating a Model\n\nTo create a Panther model, define a class that inherits from `panther.db.Model`:\n\n```python title=\"app/models.py\" linenums=\"1\"\nfrom panther.db import Model\n\nclass User(Model):\n    username: str\n    age: int\n    is_active: bool\n```\n\nPanther models inherit from both `pydantic.BaseModel` and `panther.db.Query`, giving you access to data validation and database queries.\n\n## Defining Attributes\n\nYou can define model attributes (columns) using Python type hints. Each attribute type is handled as follows:\n\n### General Types\n- **str**, **int**, **bool**: Saved in the database as-is.\n\n**Example:**\n```python title=\"app/models.py\" linenums=\"1\"\nfrom panther.db import Model\n\nclass Product(Model):\n    name: str\n    price: int\n    in_stock: bool\n```\n\n### List\n- **list**: Each item in the list is processed according to its type. The child type can be a primitive, a `pydantic.BaseModel`, or another `Model`.\n\n**Example:**\n```python title=\"app/models.py\" linenums=\"1\"\nfrom pydantic import BaseModel\nfrom panther.db import Model\n\nclass Book(BaseModel):\n    title: str\n    author: str\n    tags: list[str]\n\nclass Library(Model):\n    name: str\n    books: list[Book]\n\nclass School(Model):\n    name: str\n    libraries: list[Library]\n```\n\n### Dictionary\n- **dict**: Each value in the dictionary is processed according to its type. Only plain `dict` is supported (not typed dicts like `dict[str, int]`).\n\n**Example:**\n```python title=\"app/models.py\" linenums=\"1\"\nfrom panther.db import Model\n\nclass Config(Model):\n    settings: dict\n```\n\n### Nested Models\n- **pydantic.BaseModel**: Treated like a dictionary, but with type information for each item. Each item is processed according to its type.\n\n**Example:**\n```python title=\"app/models.py\" linenums=\"1\"\nfrom pydantic import BaseModel\nfrom panther.db import Model\n\nclass Address(BaseModel):\n    city: str\n    zipcode: str\n\nclass Customer(Model):\n    name: str\n    address: Address\n```\n\n### Foreign Keys\n- **panther.db.Model**: Treated as a foreign key relationship.\n    1. The related model's value is stored in the database.\n    2. Its `id` is saved in the main document (table).\n    3. This process is handled automatically, so you always have access to all attributes of the related model.\n    4. Panther retrieves the corresponding value from the database and returns it as a fully populated model instance.\n\n**Example:**\n```python title=\"app/models.py\" linenums=\"1\"\nfrom panther.db import Model\n\nclass Department(Model):\n    name: str\n\nclass Employee(Model):\n    name: str\n    department: Department\n```\n\n### Optional Attributes\n- You can make an attribute optional by using a union with `None` (e.g., `str | None`) and providing a default value (e.g., `= None`).\n- If you make an attribute optional, you must assign a default value.\n\n**Example:**\n```python title=\"app/models.py\" linenums=\"1\"\nfrom panther.db import Model\n\nclass Article(Model):\n    title: str\n    summary: str | None = None  # Optional attribute with default value\n```\n\n### File Types\n- **panther.file_handler.File**: Handles file uploads and provides file manipulation capabilities.\n- **panther.file_handler.Image**: Specialized File class for image uploads with automatic MIME type validation.\n\n!!! tip \"Comprehensive File Handling Guide\"\n    For detailed information about file handling, including advanced features, best practices, and troubleshooting, see the dedicated [File Handling](file_handling.md) documentation.\n\n**Example:**\n```python title=\"app/models.py\" linenums=\"1\"\nfrom datetime import datetime\nfrom panther.db import Model\nfrom panther.file_handler import File, Image\n\nclass Document(Model):\n    title: str\n    file: File\n    uploaded_at: datetime\n\nclass Profile(Model):\n    name: str\n    avatar: Image\n    bio: str | None = None\n```\n\nWhen using File types in models:\n\n- The file metadata (name, content type) is preserved\n- The file path is stored in the database\n- The actual file content is automatically saved to disk when inserting the model\n- You can access file properties and methods on the model instance\n\n**Example usage:**\n```python\n# Create a document with file\ndocument = await Document.insert_one({\n    'title': 'My Document',\n    'file': File(file_name='document.pdf', content_type='application/pdf', file=file_bytes),\n    'uploaded_at': datetime.now()\n})\n\n# Access file properties\nprint(document.file.file_name)  # 'document.pdf'\nprint(document.file.content_type)  # 'application/pdf'\nprint(document.file.size)  # File size in bytes\n\n# Read file content\nwith document.file as f:\n    content = f.read()\n```\n\n\n"
  },
  {
    "path": "docs/docs/open_api.md",
    "content": "# OpenAPI Integration in Panther\n\nPanther automatically generates an OpenAPI specification for your APIs. This makes it easy to document, test, and share your API endpoints.\n\n## How to Enable OpenAPI in Your Project\n\nPanther provides multiple OpenAPI UI options that you can import directly into your project. You don't need to add the bundled `openapi.urls.url_routing` - instead, you can choose the specific UI component you prefer.\n\n### Available OpenAPI UI Components\n\nPanther offers several OpenAPI UI options available in `panther.openapi.views`:\n\n- **`ScalarOpenAPI`** - Modern, customizable API documentation UI\n- **`SwaggerOpenAPI`** - Classic Swagger UI interface\n- **`RedocOpenAPI`** - Clean, responsive documentation interface\n- **`RapiDocOpenAPI`** - Fast, lightweight API documentation\n- **`SpotlightOpenAPI`** - Stoplight Elements integration\n\n### Adding OpenAPI to Your Project\n\nYou can import and use any of these UI components directly in your URL configuration:\n\n```python title=\"urls.py\" linenums=\"1\"\nfrom panther.openapi.views import ScalarOpenAPI, SwaggerOpenAPI, RedocOpenAPI\n\nurl_routing = {\n    'docs/scalar/': ScalarOpenAPI,    # Modern UI at /docs/scalar/\n    'docs/swagger/': SwaggerOpenAPI,  # Classic Swagger at /docs/swagger/\n    'docs/redoc/': RedocOpenAPI,      # Clean interface at /docs/redoc/\n    # Other urls\n}\n```\n\n### Alternative: Using All UI Components\n\nIf you want to include all OpenAPI UI options at once, you can still use the bundled routing:\n\n```python title=\"urls.py\" linenums=\"1\"\nfrom panther.openapi.urls import url_routing as openapi_url_routing\n\nurl_routing = {\n    'docs/': openapi_url_routing,  # Includes all UI options\n    # Other urls\n}\n```\n\nThis will make the following endpoints available:\n\n- `/docs/scalar/` - Scalar UI\n- `/docs/swagger/` - Swagger UI  \n- `/docs/redoc/` - ReDoc UI\n- `/docs/rapidoc/` - RapiDoc UI\n- `/docs/spotlight/` - Spotlight UI\n\n## How Panther Determines Response Models and Status Codes\n\nPanther follows a specific logic to generate the OpenAPI YAML for your APIs:\n\n1. **`output_schema`**: Panther first looks for an `output_schema` attribute to generate the OpenAPI documentation. This is the recommended and most accurate way to specify your response model and status code.\n2. **`output_model`**: If `output_schema` does not exist, Panther looks for an `output_model` attribute to generate the response type. It will also attempt to extract the status code from your source code.\n3. **Source Code Analysis**: If neither `output_schema` nor `output_model` is available, Panther tries to extract the response data and status code directly from your source code using static analysis with `ast`.\n\nFor best results and more accurate documentation, always specify `output_schema` in your APIs.\n\n## How Panther Generates OpenAPI Docs\n\nPanther inspects your API views for an `output_schema` attribute. This attribute should be an instance of `panther.openapi.OutputSchema`, which describes the response model and status code for your endpoint.\n\n- `model` in `OutputSchema` can be either a `pydantic.BaseModel` or a `panther.serializer.ModelSerializer`.\n- `status_code` should be an integer (e.g., `status.HTTP_200_OK`).\n\n### Example\n\n=== \"Function-Base API\"\n\n    ```python linenums=\"1\"\n    from pydantic import BaseModel\n    from panther import status\n    from panther.app import API\n    from panther.openapi import OutputSchema\n    \n    class UserSerializer(BaseModel):\n        username: str\n        age: int\n    \n    @API(output_schema=OutputSchema(model=UserSerializer, status_code=status.HTTP_200_OK))\n    def user_api():\n        ...\n    ```\n\n=== \"Class-Base API\"\n\n    ```python linenums=\"1\"\n    from pydantic import BaseModel\n    from panther import status\n    from panther.app import GenericAPI\n    from panther.openapi import OutputSchema\n    \n    class UserSerializer(BaseModel):\n        username: str\n        age: int\n    \n    class UserAPI(GenericAPI):\n        output_schema = OutputSchema(model=UserSerializer, status_code=status.HTTP_200_OK)\n        ...\n    ```\n\n---\n\n**Note:** The OpenAPI integration is currently in beta. Contributions, feedback, and ideas are very welcome!"
  },
  {
    "path": "docs/docs/query.md",
    "content": "# Querying Documents\n\nPanther ODM provides a simple, async interface for interacting with your database models. This guide covers the most common operations.\n\n---\n\n### find_one\n\nGet a single document matching the filter.\n\n```python\nfrom app.models import User\n\nuser: User = await User.find_one(id=1, name='Ali')\n# or\nuser: User = await User.find_one({'id': 1, 'name': 'Ali'})\n```\n\n- **Returns:** An instance of the model or `None` if not found.\n\n---\n\n### find\n\nGet multiple documents matching the filter.\n\n```python\nusers: Cursor = await User.find(age=18, name='Ali')\n```\n\n- **Returns:** A `Cursor` object (see [Cursor Usage](#cursor-usage)).\n\n#### Chaining\n\nYou can chain `skip()`, `limit()`, and `sort()`:\n\n```python\nusers: Cursor = await User.find(age=18).skip(10).limit(10).sort([('age', -1)])\n```\n\n---\n\n### all\n\nGet all documents in the collection.\n\n```python\nusers: Cursor = await User.all()\n```\n\n---\n\n### first / last\n\nGet the first or last document matching the filter.\n\n```python\nuser: User = await User.first(age=18)\nuser: User = await User.last(age=18)\n```\n\n---\n\n### aggregate\n\nPerform an aggregation (MongoDB only).\n\n```python\npipeline = [\n    {'$match': {...}},\n    {'$group': {...}},\n    # ...\n]\nusers: Iterable[dict] = await User.aggregate(pipeline)\n```\n\n---\n\n### count\n\nCount documents matching the filter.\n\n```python\ncount: int = await User.count(age=18)\n```\n\n---\n\n## Inserting Documents\n\n### insert_one\n\nInsert a single document.\n\n```python\nuser: User = await User.insert_one(age=18, name='Ali')\n```\n\n---\n\n### insert_many\n\nInsert multiple documents.\n\n```python\nusers = [\n    {'age': 18, 'name': 'Ali'},\n    {'age': 17, 'name': 'Saba'},\n]\nusers: list[User] = await User.insert_many(users)\n```\n\n---\n\n## Updating Documents\n\n### update\n\nUpdate the current document instance.\n\n```python\nuser = await User.find_one(name='Ali')\nawait user.update(name='Saba')\n```\n\n---\n\n### update_one / update_many\n\nUpdate one or more documents matching a filter.\n\n```python\nis_updated: bool = await User.update_one({'id': 1}, name='Ali')\nupdated_count: int = await User.update_many({'name': 'Saba'}, age=18)\n```\n\n---\n\n### save\n\nSave the document (insert or update).\n\n```python\nuser = User(name='Ali')\nawait user.save()\n```\n\n---\n\n## Deleting Documents\n\n### delete\n\nDelete the current document instance.\n\n```python\nuser = await User.find_one(name='Ali')\nawait user.delete()\n```\n\n---\n\n### delete_one / delete_many\n\nDelete one or more documents matching a filter.\n\n```python\nis_deleted: bool = await User.delete_one(age=18)\ndeleted_count: int = await User.delete_many(age=18)\n```\n\n---\n\n## Special Methods\n\n### find_one_or_insert\n\nGet or insert a document.\n\n```python\nuser, is_inserted = await User.find_one_or_insert(age=18, name='Ali')\n```\n\nFor correctness under concurrency, define a unique index/constraint on the queried fields.\n\n---\n\n### find_one_or_raise\n\nGet a document or raise `NotFoundAPIError`.\n\n```python\nuser: User = await User.find_one_or_raise(age=18)\n```\n\n---\n\n## Cursor Usage\n\n- The `find()` and `all()` methods return a `Cursor` object.\n- You can iterate over it or use it as a list.\n- For MongoDB: `from panther.db.cursor import Cursor`\n- For PantherDB: `from pantherdb import Cursor`\n\n---\n\n## Notes\n\n- All methods are async unless otherwise noted.\n- Filters can be passed as keyword arguments or as a dictionary.\n- Some features (like `aggregate`) are only available for MongoDB.\n"
  },
  {
    "path": "docs/docs/redis.md",
    "content": "# Redis Integration\n\nRedis is a fast, in-memory key-value store commonly used for caching, session management, real-time analytics, and pub/sub messaging. Panther supports Redis natively, allowing you to leverage its power for authentication, caching, throttling, and websocket pub/sub features.\n\n## Configuration\n\nTo enable Redis in Panther, fill out the `REDIS` block in your configuration file:\n\n```python\nREDIS = {\n    'class': 'panther.db.connections.RedisConnection',\n    'host': 'localhost',  # Optional, default is 'localhost'\n    'port': 6379,         # Optional, default is 6379\n    'db': 0,              # Optional, default is 0\n    # Add any other redis-py supported parameters here\n}\n```\n\n**Note:** The arguments are the same as those accepted by `redis.Redis.__init__()` from the [redis documentation](https://redis.readthedocs.io/en/latest/).\n\n## How It Works\n\n- Panther creates an asynchronous Redis connection based on the `REDIS` block you define in your configuration.\n- You can access the Redis connection via:\n\n    ```python\n    from panther.db.connections import redis\n    ```\n\n- Example usage:\n\n    ```python\n    from panther.db.connections import redis\n\n    await redis.set('name', 'Ali')\n    result = await redis.get('name')\n    print(result)\n    ```\n\n## Features Using Redis\n\n- **Authentication:** Store and retrieve JWT tokens for logout functionality.\n- **Caching:** Cache responses for faster access.\n- **Throttling:** Track and limit request rates.\n- **WebSocket:** Manage pub/sub connections for real-time features.\n"
  },
  {
    "path": "docs/docs/release_notes.md",
    "content": "# Panther Release Notes\n\n### 5.2.3 \n- Fix background task async loop mismatch with app event loop registration (#146)\n- Escape Mongodb search input before building regex (#147)\n\n### 5.2.2\n- Fix function API request isolation\n- Make `find_one_or_insert` race-safe on duplicate key conflicts\n\n### 5.2.0\n- Update requirements for `Python3.14` compatibility\n\n### 5.1.0\n- Add `FileResponse`\n\n### 5.0.2\n- Minor performance improvement.\n\n### 5.0.0\n- Add Built-In `OpenAPI`\n- Add Built-In `AdminPanel`\n- **File Handling Improvements**:\n    - Make `File.save()` method **idempotent** - multiple calls return the same path\n    - Add directory path support - paths ending with `/` are treated as directories\n    - Automatic directory creation when saving files\n    - Enhanced file path handling for both string and Path objects\n\n- Support `Set-Cookie` in Response\n- Support `application/x-www-form-urlencoded` Content Type\n- Support Different `Middlewares` Per API\n- Support `asyncio` in `panther shell`\n- Support `Boolean` type in `load_env()`\n- Add `CORSMiddleware`\n- Add `RedirectResponse`\n- Optimized `ORM` with smarter `ObjectID` handling\n- Add CLI `createuser` command\n- Deprecated:\n    - Usage of `auth` and `permissions` in `API`, `GenericAPI` and `GenericWebsocket` has been change, make sure to check the docs. \n    - `DEFAULT_CACHE_EXP` in configs.\n    - Rename `cache_exp_time` to `cache` in `@API` and `GenricAPI`.\n    - Rename `panther.throttling.Throttling` to `panther.throttling.Throttle`.\n    - Remove `MONITORING` and move its functionality as middleware in `panther.middelwares.monitoring.MonitoringMiddleware` .\n    - `BaseUser.check_password(new_password=...)` is renamed to `password`.\n    - `TemplateResponse(path=...)` is renamed to `name`.\n    - Middlewares does not support custom args for their `__init__`.\n    - New Style of Middleware declaration [[Documentation]](middlewares.md).\n    - New background task declaration, `background_tasks.add_task(BackgroundTask(...))` --> `BackgroundTask(...).submit()`.\n    - `REDIS` block in configs does not support `websocket_db` anymore.\n    - `MIDDLEWARE` does not support WebSocket middlewares anymore, you have to define them in `WS_MIDDLEWARE`. \n\n### 4.3.7\n- Improve MultiPart-FormData Regex\n\n### 4.3.6\n- Support middleware class in MIDDLEWARES\n\n### 4.3.4\n- Cache Response Headers\n\n### 4.3.4\n- Fix an issue on TemplateResponse when TEMPLATES_DIR is '.'\n\n### 4.3.3\n- Fix not provided context in TemplateResponse\n\n### 4.3.2\n- Support `Python 3.13` \n\n### 4.3.0\n- Support `Jinja2 Template Engine` \n\n### 4.2.0\n- Support `OPTIONS` method\n\n### 4.1.3\n- Add `reload()` method to queries\n- Add `get_input_model()` & `get_output_model()` to `GenericAPI`\n- Support `per_delete()` & `post_delete()` in `DeleteAPI` \n- Support mongodb operators in `update(`) & `update_one()`\n\n### 4.1.2\n- Fix some issues for `Windows`\n\n### 4.1.1\n- Fix an issue in `Response.prepare_data()` when `data` is `Cursor`\n- Split `paginate` and `template` in `Pagination`\n\n### 4.1.0\n- Support `prepare_response` in `Serializers`\n- Rename `objects()` to `cursor()`\n\n### 4.0.1\n- Fix an issue in `startup` lifespan\n\n### 4.0.0\n- Move `database` and `redis` connections from `MIDDLEWARES` to their own block, `DATABASE` and `REDIS`\n- Make `Database` queries `async`\n- Make `Redis` queries `async`\n- Add `StreamingResponse`\n- Add `generics` API classes\n- Add `login()` & `logout()` to `JWTAuthentication` and used it in `BaseUser`\n- Support `Authentication` & `Authorization` in `Websocket`\n- Rename all exceptions suffix from `Exception` to `Error` (https://peps.python.org/pep-0008/#exception-names)\n- Support `pantherdb 2.0.0` (`Cursor` Added)\n- Remove `watchfiles` from required dependencies\n- Support `exclude` and `optional_fields` in `ModelSerializer`\n- Minor Improvements\n\n### 3.9.0\n- Change the style of `ModelSerializer` usage \n\n### 3.8.2\n- Add `content-type = application/json` header in raise response of `__call__` \n\n### 3.8.1\n- Fix an issue in `_create_pantherdb_session()`\n\n### 3.8.0\n- Handle WebSocket connections when we have multiple workers with `multiprocessing.Manager`\n\n### 3.7.0\n- Add `ModelSerializer`\n\n### 3.6.0\n- Use `observable` pattern for loading database middleware and inheritance of the `Query` class\n- Remove `IDType` from the `Model`\n- Change `encrypt_password()` method, now uses `scrypt` + `md5` \n\n### 3.5.1\n- Set default behavior of `GenericWebsocket.connect` to ignore the connection (`reject`)\n\n### 3.5.0\n- Add `WebsocketTestClient`\n\n### 3.4.0\n- Support `WebsocketMiddleware`\n\n### 3.3.2\n- Add `content-length` to response header\n\n### 3.3.1\n- Check `ruff` installation on startup\n- Fix an issue in `routing`\n\n### 3.3.0\n- Add Auto Reformat Code\n\n### 3.2.4\n- Add all() query\n- Add tests for `pantherdb`, `load_configs()`, `status.py`, `Panel`, `multipart`, `request headers`\n- Refactor `Headers()` class\n- Check `uvloop` installation on `Panther init`\n- Minor Improvement\n\n### 3.2.1\n- Move `Startup` to `__call__`\n\n### 3.2.0\n- Support `Startup` & `Shutdown` Events\n\n### 3.1.5\n- Support `Websocket` in the `monitoring` \n- Refactor `collect_all_models()`\n\n### 3.1.4\n- Check ws redis connection on the `init`\n- Refactor `Monitoring` class and usage\n- Improve `logging` config\n- Check database connection before query\n\n### 3.1.3\n- Add `Image` base class \n- Add `size` to `File` base class\n- Improve the way of loading `configs` in `single-file` structure\n- Improve `background_tasks.py`, `generate_ws_connection_id()`\n- `bpython` removed from being the default python shell\n- Improve `load_middlewares()` error handling \n- Print `configs` on the `run`\n- Add `requirements.txt` for development \n- Update `roadmap.jpg`, `README.md`\n \n### 3.1.2\n- Add new methods to `BackgroundTask`\n  - `every_seconds()`\n  - `every_minutes()`\n  - `every_hours()`\n  - `every_days()`\n  - `every_weeks()`\n  - `at()`\n\n### 3.1.1\n- Upgrade `PantherDB` version\n- Add `first()`, `last()` queries\n\n### 3.1.0\n- Add `BackgroundTasks`\n\n### 3.0.3\n- Add `find_one_or_raise` query\n- Add `last_login` to `BaseUser`\n- Add `refresh_life_time` to `JWTConfig`\n- Add `encode_refresh_token()` to `JWTAuthentication`\n- Add `encrypt_password()`\n- Handle `PantherException`\n- Handle `RedisConnection` without `connection_pool`\n\n### 3.0.2\n- Added 'utf-8' encoding while opening the file \"README.md\" in setup.py\n- Fixed panther shell not working issue in windows.\n- Added a condition to raise error if no argument is passed to panther command in cli.\n\n### 3.0.1\n- Assume content-type is 'application/json' if it was empty\n- Fix an issue on creating instance of model when query is done\n\n### 3.0.0\n- Support **Websocket**\n- Implement **Built-in TestClient**\n- Support Single-File Structure\n- Support `bytes` as `Response.data`\n- Add `methods` to `API()`\n- Change `Request.pure_data` to `Request.data`\n- Change `Request.data` to `Request.validated_data`\n- Change `panther.middlewares.db.Middleware` to `panther.middlewares.db.DatabaseMiddleware`\n- Change `panther.middlewares.redis.Middleware` to `panther.middlewares.redis.RedisMiddleware`\n- Fix `panther run` command\n- Minor Improvement\n\n### 2.4.2\n- Don't log content-type when it's not supported\n\n### 2.4.1\n- Fix an issue in collect_all_models() in Windows\n\n### 2.4.0\n- Handle Complex Multipart-FormData\n\n### 2.3.3\n- Fix a bug in response headers\n\n### 2.3.2\n- Fix a bug in Template\n\n### 2.3.1\n- Handle PlainTextResponse\n- Handle Custom Header in Response\n- Change the way of accepting 'URLs' in configs (relative -> dotted)\n- Fix an issue in collect_all_models()\n\n### 2.3.0\n- Handle HTMLResponse\n\n### 2.2.0\n- Supporting File \n\n### 2.1.6\n- Fix validation errors on nested inputs\n\n### 2.1.5\n- Fix response of nested Models in _panel/<index>/\n\n### 2.1.4\n- Add access-control-allow-origin to response header\n\n### 2.1.3\n- Upgrade greenlet version in requirements for python3.12\n\n### 2.1.2\n- Add ruff.toml\n- Add Coverage to workflows\n- Fix a bug for running in Windows\n\n### 2.1.1\n- Fix a bug in main.py imports\n\n### 2.1.0\n- Support Sync APIs\n\n### 2.0.0\n- Supporting class-base APIs\n\n### 1.7.20\n- Fix an issue in find_endpoint()\n\n### 1.7.19\n- Fix an issue in routing\n- Fix an issue on return complex dict Response\n\n### 1.7.18\n- Remove uvloop from requirements for now (we had issue in windows)\n\n### 1.7.16\n- Trying to fix requirements for windows\n- Minor improvement in BaseMongoDBQuery\n\n### 1.7.15\n- Fix an issue in handling form-data\n\n### 1.7.14\n- Add Cache and Throttling doc to FirstCrud\n- Fix an issue in BasePantherDBQuery._merge() \n\n### 1.7.13\n- Hotfix validation of _id in Model()\n\n### 1.7.12\n- Fix a bug in routing\n\n### 1.7.11\n- Fix an issue in template\n\n### 1.7.10\n- Fix a bug in `collect_urls` and rename it to `flatten_urls`\n- Add General Tests\n- Compatible with python3.10 (Not Tested)\n- Working on docs\n\n### 1.7.9\n- Working on doc\n\n### 1.7.8\n- Fix a bug\n- Update docs\n\n### 1.7.8\n- Fix a bug\n- Update docs\n\n### 1.7.7\n- Fix a bug\n \n### 1.7.5\n- Change the way of raising exception in JWTAuthentication\n- Rename User model to BaseUser\n- Fix template\n \n### 1.7.4\n- Crop Logo\n\n### 1.7.3\n- Add Throttling Doc\n- Fix some issue in Doc\n\n### 1.7.2\n- Add Throttling to example\n- Customize install_requires in setup.py\n- Improve monitoring cli command\n\n### 1.7.1\n- Rename db BaseModel to Model \n- Add more docs\n\n### 1.7.0\n- Add Throttling\n\n### 1.6.1\n- Add AdminPermission\n\n### 1.6.0\n- Handle Permissions\n\n### 1.5.2\n- Improve Response data serialization\n- Fix a bug in JWTAuthentication\n\n### 1.5.1\n- Fix error messages\n\n### 1.5.0\n- Refactor Mongodb ODM\n- Minor Improvement\n\n### 1.4.0\n- Add QUERY_LOG\n\n### 1.3.2\n- Add Uvicorn to the setup requirements\n- Update Readme\n\n### 1.3.1\n- Fix a bug in project creation template\n- Fix a bug in caching\n\n### 1.3.0\n- Add PantherDB to Panther\n- Remove tinydb\n\n### 1.2.7\n- Fix a bug while using tinydb\n\n### 1.2.6\n- Update Readme\n\n### 1.2.5\n- Fix install_requires issue\n- Add benchmarks to docs\n\n### 1.2.4\n- Remove Uvicorn From install_requires\n- Working on Docs\n\n### 1.2.3\n- Fix URL Routing\n\n### 1.2.1\n- Path Variable Handled Successfully\n\n### 1.2.0\n- Read multipart/form-data with Regex\n\n### 1.1.9\n- Refactoring code style with ruff \n- Add asyncio.TaskGroup() \n\n### 1.1.8\n- Refactor cli run command \n\n### 1.1.7\n- Add benchmark pictures to doc \n\n### 1.1.5\n- Clean Readme\n- Clean main.py \n\n### 1.1.4\n- Update Readme \n\n### 1.1.3\n- Fix a query in TinyDB \n\n### 1.1.2\n- Add delete_many query to TinyDB \n\n### 1.1.1\n- Add TinyDB\n\n### 1.1.0\n- Debug the Template \n\n### 1.0.9\n- Handle string exceptions (raise them as detail: error) \n- Little debug on MongoQueries\n\n### 1.0.7\n- Working on queries\n- Fix a bug in query methods \n\n### 1.0.6\n- Update the Template \n\n### 1.0.4\n- Debug template \n\n### 1.0.2\n- Add global config\n- Split the BaseModels\n- Worked on MongoQuery\n- Set Mongo as default database while creating project \n- Minor Improvement\n\n### 1.0.1\n- Add alembic To Project \n\n### 1.0.\n- Refactor & Complete the CLI \n\n### 0.1.9\n-  Fix install_requires\n\n### 0.1.8\n- Update Readme\n\n### 0.1.7\n- Update Readme\n\n### 0.1.6\n- Handle Most Types as Data in Response\n\n### 0.1.4\n- Working On DB Connection \n\n### 0.0.1\n- Make It Ready For PyPI \n\n"
  },
  {
    "path": "docs/docs/roadmap.md",
    "content": "# Roadmap\n\n## Current Status\n\n![Roadmap](https://raw.githubusercontent.com/AliRn76/panther/master/docs/docs/images/roadmap.jpg)\n\n## Contributing\n\nWe welcome contributions! Here's how you can help:\n\n1. Report bugs and suggest features through [GitHub Issues](https://github.com/AliRn76/panther/issues)\n2. Submit pull requests for bug fixes and new features\n3. Improve documentation\n4. Share your experience and use cases\n5. Help with testing and quality assurance\n\n## Community Feedback\n\nYour input is valuable! If you have any feature requests or suggestions for future development, please:\n\n1. Check existing issues to avoid duplicates\n2. Create a new issue with a clear description\n3. Include use cases and potential implementation ideas\n4. Label the issue appropriately\n"
  },
  {
    "path": "docs/docs/ruff.md",
    "content": "# Auto-Reformatting with Ruff\n\nPanther supports automatic code reformatting using [Ruff](https://docs.astral.sh/ruff/), a fast Python linter and formatter written in Rust.\n\n## Quick Setup\n\nTo enable automatic code reformatting, set `AUTO_REFORMAT` to `True` in your configuration:\n\n```python\nAUTO_REFORMAT = True  # Default is False\n```\n\n## How It Works\n\nWhen `AUTO_REFORMAT` is enabled, Panther will automatically reformat your code:\n\n- **On every application run** - Code is reformatted when you start your application.\n- **With `--reload` flag** - Code is reformatted on every file change during development\n\nThis ensures your codebase maintains consistent formatting standards automatically.\n\n## Installation\n\nThe auto-reformatting feature requires the Ruff package. Install it using pip:\n\n```bash\npip install ruff\n```\n\n## Configuration\n\nYou can create a custom `ruff.toml` file in your project root to configure formatting rules:\n\n```toml title=\"ruff.toml\" linenums=\"1\"\n# Set the maximum line length\nline-length = 120\n\n# Set the indentation style\nindent-width = 4\n\n# Enable/disable specific rules\nselect = [\"E\", \"F\", \"I\"]\nignore = [\"E501\"]\n\n[format]\n# Formatting options\nquote-style = \"single\"\nindent-style = \"space\"\nskip-magic-trailing-comma = false\nline-ending = \"auto\"\n```\n\n## Benefits\n\n- **Consistent Code Style**: Automatic formatting ensures all code follows the same style\n- **Time Saving**: No need to manually format code or run formatters separately\n- **Team Collaboration**: Everyone on the team gets consistent formatting automatically\n- **Fast Performance**: Ruff is extremely fast, making the formatting process seamless\n\n## References\n\n- [Ruff Documentation](https://docs.astral.sh/ruff/)\n- [Ruff Formatter Configuration](https://docs.astral.sh/ruff/formatter/)\n- [Ruff Rules Reference](https://docs.astral.sh/ruff/rules/)\n"
  },
  {
    "path": "docs/docs/serializer.md",
    "content": "# Panther Serializers\n\nPanther provides flexible ways to define serializers for your APIs. Serializers are responsible for validating and transforming input data. This guide covers the three supported styles and when to use each.\n\n---\n\n## Introduction\n\n**serializer** in Panther is a class that defines how input data is validated and (optionally) transformed before being processed by your API logic. Panther supports three main styles:\n\n- **Pydantic Serializer**: Use a standard Pydantic model.\n\n- **ModelSerializer**: Generate fields from a Panther model.\n\n- **ModelSerializer + Pydantic**: Combine model-based fields with Pydantic features and custom validation.\n\n---\n\n## Style 1: Pydantic Serializer\n\nUse a regular Pydantic class as your serializer. This is the most direct approach and is ideal for simple use cases or when you want full control over the fields.\n\n```python linenums=\"1\"\nfrom pydantic import BaseModel, Field\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\n\nclass UserSerializer(BaseModel):\n    username: str\n    password: str\n    first_name: str = Field(default='', min_length=2)\n    last_name: str = Field(default='', min_length=4)\n\n@API(input_model=UserSerializer)\nasync def serializer_example(request: Request):\n    return Response(data=request.validated_data)\n```\n\n---\n\n## Style 2: ModelSerializer\n\nUse Panther's `ModelSerializer` to automatically generate serializer fields from your model. This is useful for DRY code and consistency between your models and serializers.\n\n```python linenums=\"1\"\nfrom pydantic import Field\nfrom panther.app import API\nfrom panther.db import Model\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.serializer import ModelSerializer\n\nclass User(Model):\n    username: str\n    password: str\n    first_name: str = Field(default='', min_length=2)\n    last_name: str = Field(default='', min_length=4)\n\n# Option 1: Specify fields explicitly\nclass UserModelSerializer(ModelSerializer):\n    class Config:\n        model = User\n        fields = ['username', 'first_name', 'last_name']\n        required_fields = ['first_name']\n\n# Option 2: Exclude specific fields\nclass UserModelSerializer(ModelSerializer):\n    class Config:\n        model = User\n        fields = '*'\n        required_fields = ['first_name']\n        exclude = ['id', 'password']\n\n@API(input_model=UserModelSerializer)\nasync def model_serializer_example(request: Request):\n    return Response(data=request.validated_data)\n```\n\n---\n\n## Style 3: ModelSerializer with Pydantic Features\n\nCombine `ModelSerializer` with Pydantic features for advanced use cases. This allows you to add custom fields, validators, and configuration.\n\n```python linenums=\"1\"\nfrom pydantic import Field, field_validator, ConfigDict\nfrom panther.app import API\nfrom panther.db import Model\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.serializer import ModelSerializer\n\nclass User(Model):\n    username: str\n    password: str\n    first_name: str = Field(default='', min_length=2)\n    last_name: str = Field(default='', min_length=4)\n\nclass UserModelSerializer(ModelSerializer):\n    model_config = ConfigDict(str_to_upper=True)\n    age: int = Field(default=20)\n    is_male: bool\n    username: str\n\n    class Config:\n        model = User\n        fields = ['first_name', 'last_name']\n        required_fields = ['first_name']\n        optional_fields = ['last_name']\n\n    @field_validator('username')\n    def validate_username(cls, username):\n        print(f'{username=}')\n        return username\n\n@API(input_model=UserModelSerializer)\nasync def model_serializer_example(request: Request):\n    return Response(data=request.validated_data)\n```\n\n---\n\n## Comparison Table\n\n| Feature                | Pydantic Serializer | ModelSerializer | ModelSerializer + Pydantic |\n|------------------------|:------------------:|:--------------:|:-------------------------:|\n| Model-based fields     |         ❌          |       ✅        |            ✅             |\n| Custom fields          |         ✅          |       ❌        |            ✅             |\n| Pydantic validators    |         ✅          |       ❌        |            ✅             |\n| Field inclusion/exclude|         Manual      |   Configurable |        Configurable       |\n| Best for               |  Simple cases      |  DRY, model-aligned | Advanced/Hybrid      |\n\n---\n\n## Notes & Best Practices\n\n- `ModelSerializer` uses your model's field types and default values for validation.\n- `Config.model` and `Config.fields` are required for `ModelSerializer`.\n- Use `Config.required_fields` to force fields to be required.\n- Use `Config.optional_fields` to force fields to be optional.\n- A field cannot be in both `required_fields` and `optional_fields`.\n- If you use `required_fields` or `optional_fields`, those fields must also be listed in `fields`.\n- You can use `'*'` for `fields`, `required_fields`, or `optional_fields` to include all model fields.\n- `Config.exclude` is useful when `fields` is set to `'*'`.\n- You can add custom fields and validators when combining `ModelSerializer` with Pydantic features.\n\n---\n\n## File Handling in Serializers\n\nWhen working with file uploads, Panther's `File` and `Image` classes integrate seamlessly with serializers.\n\n!!! tip \"Comprehensive File Handling Guide\"\n    For detailed information about file handling, including advanced features, best practices, and troubleshooting, see the dedicated [File Handling](file_handling.md) documentation.\n\n### Basic File Serialization\n\n```python title=\"app/serializers.py\" linenums=\"1\"\nfrom panther.serializer import ModelSerializer\n\nclass FileUploadSerializer(ModelSerializer):\n    class Config:\n        model = FileUpload\n        fields = ['file', 'description']\n        required_fields = ['file']\n\nclass ImageUploadSerializer(ModelSerializer):\n    class Config:\n        model = ImageUpload\n        fields = ['image', 'title']\n        required_fields = ['image']\n```\n\n### File Validation\n\n```python title=\"app/serializers.py\" linenums=\"1\"\nfrom pydantic import field_validator\nfrom panther import status\nfrom panther.exceptions import APIError\nfrom panther.file_handler import File\nfrom panther.serializer import ModelSerializer\n\nclass DocumentUploadSerializer(ModelSerializer):\n    class Config:\n        model = DocumentUpload\n        fields = ['file', 'title']\n        required_fields = ['file']\n\n    @field_validator('file')\n    @classmethod\n    def validate_file_size(cls, file: File) -> File:\n        if file.size > 10 * 1024 * 1024:  # 10MB limit\n            raise APIError(\n                detail='File size must be less than 10MB',\n                status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE\n            )\n        return file\n```\n\n### File Properties\n\nWhen working with `File` objects in serializers, you have access to:\n\n- `file.file_name`: The original filename\n- `file.content_type`: The MIME type\n- `file.size`: File size in bytes\n- `file.file`: The file content as bytes"
  },
  {
    "path": "docs/docs/single_file.md",
    "content": "# Using Panther in a Single-File Structure\n\nIf you prefer to work with `Panther` in a single Python file, follow the steps below to quickly set up and run your API.\n\n## Step 1: Define Your APIs\n\nWrite your API endpoints as usual using the `@API()` decorator or inheriting from `GenericAPI`:\n\n```python title=\"main.py\" linenums=\"1\"\nfrom panther.app import API\n\n@API()\nasync def hello_world_api():\n    return {'detail': 'Hello World'}\n```\n\n## Step 2: Create a URL Routing Dictionary\n\nMap your endpoints to their respective URLs in a dictionary (commonly named `url_routing`):\n\n```python title=\"main.py\" linenums=\"6\"\nurl_routing = {\n    '/': hello_world_api,\n}\n```\n\n## Step 3: Initialize the Panther App\n\nCreate an instance of the `Panther` app, passing your current module name and the URL routing dictionary:\n\n```python title=\"main.py\" linenums=\"1\"\nfrom panther import Panther\nfrom panther.app import API\n\n@API()\nasync def hello_world_api():\n    return {'detail': 'Hello World'}\n\nurl_routing = {\n    '/': hello_world_api,\n}\n\napp = Panther(__name__, configs=__name__, urls=url_routing)\n```\n\n## Step 4: Run Your Project\n\nUse the following command to start your application:\n\n```bash\npanther run main:app\n```\n\n---\n\n## Additional Notes\n\n- The `urls` parameter is required unless you provide the URLs via configuration.\n- When you pass `configs=__name__` to the `Panther` constructor, Panther will load the configuration from the current file. If you omit this, Panther defaults to loading configuration from `core/configs.py`.\n"
  },
  {
    "path": "docs/docs/templates.md",
    "content": "# Template Responses\n\nPanther provides `TemplateResponse` for serving HTML templates with dynamic content. This guide explains how to configure and use templates in your Panther application.\n\n## Configuration\n\nTo use `TemplateResponse` with template files, you must configure the `TEMPLATES_DIR` setting in your application configuration.\n\n### Setting Templates Directory\n\nThe `TEMPLATES_DIR` can be a single string or a list of strings representing template directory paths:\n\n```python\n# Single directory\nTEMPLATES_DIR = 'templates/'\n\n# Multiple directories (searched in order)\nTEMPLATES_DIR = ['templates/', 'app/templates/', 'shared/templates/']\n```\n\n**Default value:** `'./'` (current directory)\n\n## Usage\n\n### Using Template Files (Recommended)\n\nWhen you have template files, use the `name` parameter to specify the template file:\n\n```python linenums=\"1\"\nfrom panther.app import API\nfrom panther.response import TemplateResponse\n\n\n@API()\ndef my_html():\n    return TemplateResponse(\n        name='index.html', \n        context={'name': 'Ali', 'title': 'Welcome'}\n    )\n```\n\n**Benefits:**\n\n- Cleaner code separation\n- Template reusability\n- Better maintainability\n\n### Using Inline HTML Content\n\nFor simple cases or when you need to generate HTML dynamically, you can pass the HTML content directly:\n\n```python linenums=\"1\"\nfrom panther.app import API\nfrom panther.response import TemplateResponse\n\n\n@API()\ndef my_html():\n    html_content = open('index.html', 'r').read()\n    return TemplateResponse(\n        source=html_content, \n        context={'name': 'Ali', 'title': 'Welcome'}\n    )\n```\n\n**Note:** This approach requires you to manage the HTML content manually and doesn't provide the benefits of template files.\n\n## Template Context\n\nThe `context` parameter allows you to pass variables to your templates:\n\n```python linenums=\"1\"\nfrom panther.app import API\nfrom panther.response import TemplateResponse\n\ndef get_user(user_id: int):\n    return ...\n\n@API()\ndef user_profile(user_id: int):\n    user = get_user(user_id)  # Your user fetching logic\n    return TemplateResponse(\n        name='profile.html',\n        context={\n            'user': user,\n            'page_title': f'{user.name}\\'s Profile',\n            'is_admin': user.role == 'admin'\n        }\n    )\n```\n\n## Example Project Structure\n\n```\nmy_panther_app/\n├── core/\n│   ├── __init__.py\n│   ├── configs.py\n│   └── urls.py\n├── app/\n│   ├── __init__.py\n│   ├── urls.py\n│   └── views.py\n├── templates/\n│   ├── base.html\n│   ├── index.html\n│   └── profile.html\n└── main.py\n```\n\nWith this structure, your configuration would be:\n\n```python title=\"core/configs.py\"\nTEMPLATES_DIR = 'templates/'\n```\n \n"
  },
  {
    "path": "docs/docs/timezone.md",
    "content": "# Timezone Configuration\n\nPanther provides built-in timezone support to ensure consistent datetime handling across your application.\n\n## Configuration\n\nYou can specify your timezone in the configuration using the `TIMEZONE` setting:\n\n```python\nTIMEZONE = 'UTC'  # Options are available in `pytz.all_timezones`\n```\n\nTo see all available timezone options, you can run:\n\n```python\nimport pytz\nprint(pytz.all_timezones)\n```\n\nThis will show you the complete list of timezone identifiers you can use in your `TIMEZONE` configuration.\n\n\n## Using Panther's Timezone-Aware DateTime\n\nPanther provides a utility function `panther.utils.timezone_now()` that returns the current datetime relative to your configured timezone:\n\n```python\nfrom panther.utils import timezone_now\n\n# Get current datetime in your configured timezone\ncurrent_time = timezone_now()\nprint(current_time)  # 2024-01-15 14:30:00+00:00 (if TIMEZONE='UTC')\n```\n\n## Where It's Used\n\nThe `timezone_now()` function is automatically used in several Panther components:\n\n- **User Authentication**: `BaseUser.date_created` and `BaseUser.last_login` use timezone-aware timestamps\n- **Background Tasks**: DateTime checking for scheduled and queued tasks\n\n> Use `timezone_now()` Instead of `datetime.now()`, to maintain consistency across your application.\n"
  },
  {
    "path": "docs/docs/urls.md",
    "content": "# URL Configuration in Panther\n\nPanther requires you to define your application's URL routing. You can provide this configuration in two ways:\n\n1. **Global Config:** Specify a dotted path to a dictionary of URLs in your configuration file.\n2. **Direct Argument:** Pass the URL dictionary directly to the `Panther(...)` constructor (commonly used in single-file applications).\n\n## How to Configure URLs\n\n- The `URLs` config should be a dotted path (string) pointing to your root URL dictionary.\n    - Example: `URLs = 'path.to.module.url_dict'`\n- The target of `URLs` **must** be a Python `dict`.\n- In the URL dictionary, each **key** is a URL path, and each **value** is either an endpoint (function/class) or another nested dictionary for sub-routing.\n\n### Path Variables\n\nYou can define variables in your URL paths using angle brackets (`<variable_name>`):\n\n- Example path: `user/<user_id>/blog/<title>/`\n- The corresponding endpoint must accept parameters with the same names.\n- Panther will automatically cast arguments to the expected types and raise an error if the types do not match.\n\n=== \"Function-based\"\n    ```python title=\"app/apis.py\"\n    @API()\n    async def profile_api(user_id: int, title: str, is_alive: bool):\n        ...\n    ```\n\n=== \"Class-based\"\n    ```python title=\"app/apis.py\"\n    class MyAPI(GenericAPI):\n        async def get(self, user_id: int, title: str, is_alive: bool):\n            ...\n    ```\n\n---\n\n## Example: Global Config Structure\nSpecify a dotted path to a dictionary of URLs in your configuration file.\n\n```\n.\n├── app\n│     ├── apis.py\n│     └── urls.py\n└── core\n      ├── configs.py\n      └── urls.py\n```\n\n```python title=\"core/configs.py\"\nURLs = 'core.urls.url_routing'\n```\n\n```python title=\"core/urls.py\" linenums=\"1\"\nfrom app.urls import app_urls\n\nurl_routing = {\n    'user/': app_urls,\n}\n```\n\n```python title=\"app/urls.py\" linenums=\"1\"\nfrom app.apis import *\n\napp_urls = {\n    'login/': login_api,\n    'logout/': logout_api,\n    '<user_id>/blog/<title>/<is_alive>/': profile_api,\n}\n```\n\n```python title=\"app/apis.py\" linenums=\"1\"\nfrom panther import API\n\n@API()\nasync def profile_api(user_id: int, title: str, is_alive: bool):\n    ...\n```\n\n---\n\n## Example: Single-File Structure\n\nYou can pass the URL dictionary directly to Panther:\n\n```python title=\"main.py\"\n# Other codes ...\n\nurls = {\n    '/': first_api,\n}\n\napp = Panther(__name__, configs=__name__, urls=urls)\n```\n"
  },
  {
    "path": "docs/docs/user_model.md",
    "content": "# User Model Configuration\n\nYou can specify which model should be used as your User model by setting the `USER_MODEL` variable in your configuration files.\n\nThe value of `USER_MODEL` should be the import path (address) of a model class that inherits from `panther.db.models.BaseUser`. Panther relies on this model for user authentication and management within the AdminPanel and all built-in authentication classes.\n\n## Usage\n\n- **Login and Authentication in AdminPanel:** The specified User model will be used to authenticate users accessing the AdminPanel.\n- **Built-in Authentication Classes:** All built-in authentication mechanisms will utilize this User model for user-related operations.\n\n## Default Value\n\nIf you do not specify a `USER_MODEL`, Panther will use `panther.db.models.BaseUser` as the default User model.\n\n## Example\n\n```python\n# In your configuration file\nUSER_MODEL = 'your_app.models.CustomUser'\n```\n"
  },
  {
    "path": "docs/docs/websocket.md",
    "content": "# WebSocket Support in Panther\n\n**WebSockets** enable you to build interactive, real-time features such as chat, notifications, and live updates.\n\n---\n\n## Structure & Requirements\n\n### Creating a WebSocket Class\nCreate a WebSocket handler class in `app/websockets.py` by inheriting from `GenericWebsocket`:\n\n```python title=\"app/websockets.py\" linenums=\"1\"\nfrom panther.websocket import GenericWebsocket\n\nclass BookWebsocket(GenericWebsocket):\n    async def connect(self):\n        await self.accept()\n        print(f'{self.connection_id=}')\n\n    async def receive(self, data: str | bytes):\n        # Echo the received message back to the client\n        await self.send(data=data)\n```\n\n### Registering WebSocket URLs\nRegister your WebSocket class in `app/urls.py`:\n\n```python title=\"app/urls.py\" linenums=\"1\"\nfrom app.websockets import BookWebsocket\n\nurls = {\n    'ws/book/': BookWebsocket,\n}\n```\n\n> Panther supports WebSocket routing just like APIs.\n\n---\n\n## How It Works\n1. **Client Connection:** The client connects to your `ws/book/` URL using the WebSocket protocol.\n2. **Connection Handling:** The `connect()` method of your WebSocket class is called.\n3. **Validation:** You can validate the connection using `self.headers`, `self.query_params`, etc.\n4. **Accept/Reject:** Accept the connection with `self.accept()`. If not accepted, it is rejected by default.\n5. **Connection ID:** Each connection gets a unique `connection_id` (accessible via `self.connection_id`). You may want to store this in a database or cache.\n6. **Receiving Messages:** Incoming messages are handled by the `receive()` method. Messages can be `str` or `bytes`.\n7. **Sending Messages:**\n    - **Within the WebSocket class:** Use `self.send(data)`.\n    - **Outside the WebSocket class:** Use `send_message_to_websocket()`:\n      ```python\n      from panther.websocket import send_message_to_websocket\n      await send_message_to_websocket(connection_id='7e82d57c9ec0478787b01916910a9f45', data='New Message From WS')\n      ```\n\n---\n\n## Advanced Usage\n\n### Authentication\nYou can enable authentication in your WebSocket class by setting `auth` to an async function or a class with an async `__call__` method. Panther will use this callable to authenticate the user. \n\n- If you do not set `auth`, Panther will use the default `WS_AUTHENTICATION` from your configuration **only if the request contains an authorization header/ cookie/ param/ etc.**. \n- If there is no authorization header, authentication is bypassed and `self.user` will be `None`.\n\nThere are several built-in options, but we recommend `QueryParamJWTAuthentication` for WebSocket authentication.\n\n```python\nWS_AUTHENTICATION = 'panther.authentications.QueryParamJWTAuthentication'\n```\n\nThis will set `self.user` to a `UserModel` instance or `None`. The connection will be rejected if any exception occurs during authentication.\n\n```python title=\"app/websockets.py\" linenums=\"1\"\nfrom panther.websocket import GenericWebsocket\nfrom app.authentications import MyAuthenticationClass\n\nclass MyWebSocket(GenericWebsocket):\n    auth = MyAuthenticationClass  # Or use an async function\n    \n    async def connect(self):\n        print(self.user)\n        ...\n```\n\n> **Note:** When authentication is bypassed (no authorization header), `self.user` will be `None` and you must rely on permissions to check the user and their authorization.\n\n### Permissions\nYou can implement your authorization logic using permission classes or functions. Any async function or class with an async `__call__` method can be used as a permission. Panther will call each permission (asynchronously). \n\n- If any return `False`, the connection will be rejected.\n\nPass a list of permission callables to your WebSocket class. \n\n- If you pass a single permission, it will be automatically wrapped in a list.\n\n> Each permission must be async (either an async function or a class with an async `__call__`).\n\n**Example Permission Function:**\n\n```python title=\"app/permissions.py\" linenums=\"1\"\nfrom panther.websocket import Websocket\n\nasync def custom_permission(request: Websocket) -> bool:\n    return True\n```\n\n**Example Permission Class:**\n\n```python title=\"app/permissions.py\" linenums=\"1\"\nfrom panther.websocket import Websocket\nfrom panther.permissions import BasePermission\n\nclass CustomPermission(BasePermission):\n    async def __call__(self, request: Websocket) -> bool:\n        return True\n```\n\n```python title=\"app/websockets.py\" linenums=\"1\"\nfrom panther.websocket import GenericWebsocket\nfrom app.permissions import custom_permission, CustomPermission\n\nclass MyWebSocket(GenericWebsocket):\n    permissions = [custom_permission, CustomPermission]  # Or just one\n    \n    async def connect(self):\n        ...\n```\n\n### Multiple Workers & Redis\n- **Recommended:** For running WebSockets with multiple workers, add Redis to your configuration. [See Adding Redis](redis.md)\n- **Without Redis:** If you do not use Redis but want to run WebSockets with multiple workers (e.g., with Gunicorn), use the `--preload` flag:\n  ```shell\n  gunicorn -w 10 -k uvicorn.workers.UvicornWorker main:app --preload\n  ```\n- **Uvicorn Limitation:** WebSockets do not work properly when using uvicorn directly with the `--workers` flag (e.g., `uvicorn main:app --workers 4`). This is because each worker process maintains its own separate WebSocket connections, and there's no shared state between workers. Use Gunicorn with the `--preload` flag or add Redis for proper WebSocket support with multiple workers.\n\n### Closing Connections\n- **Within the WebSocket class:**\n  ```python\n  from panther import status\n  await self.close(code=status.WS_1000_NORMAL_CLOSURE, reason='Closing connection')\n  ```\n- **Outside the WebSocket class:**\n  ```python\n  from panther import status\n  from panther.websocket import close_websocket_connection\n  await close_websocket_connection(connection_id='7e82d57c9ec0478787b01916910a9f45', code=status.WS_1008_POLICY_VIOLATION, reason='Closing connection')\n  ```\n\n### Path Variables\nYou can define path variables in your WebSocket URL. These will be passed to the `connect()` method:\n\n```python linenums=\"1\"\nfrom panther.websocket import GenericWebsocket\n\nclass UserWebsocket(GenericWebsocket):\n    async def connect(self, user_id: int, room_id: str):\n        await self.accept()\n\nurls = {\n    '/ws/<user_id>/<room_id>/': UserWebsocket\n}\n```\n\n---\n\n## Example\n\n### Example Client Code\n**Here's a simple example using JavaScript:**\n\n```js title=\"websocket.js\" linenums=\"1\"\nconst ws = new WebSocket('ws://127.0.0.1:8000/ws/book/');\nws.onopen = () => {\n    ws.send('Hello, server!');\n};\nws.onmessage = (event) => {\n    console.log('Received:', event.data);\n};\nws.onclose = () => {\n    console.log('Connection closed');\n};\n```\n\n### Echo Example\n**Full echo example with WebSocket:**\n\n```python title=\"main.py\" linenums=\"1\"\nfrom panther import Panther\nfrom panther.app import GenericAPI\nfrom panther.response import HTMLResponse\nfrom panther.websocket import GenericWebsocket\n\nclass EchoWebsocket(GenericWebsocket):\n    async def connect(self, **kwargs):\n        await self.accept()\n\n    async def receive(self, data: str | bytes):\n        await self.send(data)\n\nclass MainPage(GenericAPI):\n    def get(self):\n        template = \"\"\"\n        <input id=\"msg\"><button onclick=\"s.send(msg.value)\">Send</button>\n        <ul id=\"log\"></ul>\n        <script>\n            const s = new WebSocket('ws://127.0.0.1:8000/ws');\n            s.onmessage = e => log.innerHTML += `<li><- ${msg.value}</li><li>-> ${e.data}</li>`;\n        </script>\n        \"\"\"\n        return HTMLResponse(template)\n\nurl_routing = {\n    '': MainPage,\n    'ws': EchoWebsocket,\n}\napp = Panther(__name__, configs=__name__, urls=url_routing)\n```\n**Run** with `panther run main:app` and **visit** `http://127.0.0.1:8000`.\n\n---\n\n## Tips & Notes\n- **Connection Validation:** Always validate connections in `connect()` using headers or query parameters as needed.\n- **Connection IDs:** Store `connection_id` if you need to send messages to clients outside the WebSocket class.\n- **Multiple Workers:** Use Redis for scaling WebSockets across multiple workers.\n- **Error Handling:** Implement error handling in your WebSocket methods for production use.\n- **Security:** Always validate and sanitize incoming data.\n\n---\n\nEnjoy building with Panther WebSockets!\n\n"
  },
  {
    "path": "docs/mkdocs.yml",
    "content": "site_name: Panther\nsite_description: Fast &  Friendly, Web Framework For Building Async APIs\nsite_url: https://pantherpy.github.io\n\nrepo_name: AliRn76/Panther\nrepo_url: https://github.com/AliRn76/Panther\nedit_uri: edit/master/docs/\n\ntheme:\n  name: 'material'\n  custom_dir: docs\n  palette:\n    - scheme: 'default'\n      media: '(prefers-color-scheme: light)'\n      primary: 'blue'\n      toggle:\n        icon: 'material/lightbulb'\n        name: 'Switch to dark mode'\n    - scheme: 'slate'\n      media: '(prefers-color-scheme: dark)'\n      primary: 'blue'\n      toggle:\n        icon: 'material/lightbulb-outline'\n        name: 'Switch to light mode'\n  features:\n    - navigation.path\n    - navigation.footer\n    - content.code.copy\n    - content.code.annotate\n  icon:\n    repo: fontawesome/brands/github\n\nnav:\n  - Introduction: 'index.md'\n  - First CRUD: 'first_crud.md'\n  - APIs: 'api.md'\n  - Serializers: 'serializer.md'\n  - URLs: 'urls.md'\n  - Models: 'model.md'\n  - File Handling: 'file_handling.md'\n  - Database: 'database.md'\n  - Query: 'query.md'\n  - Redis: 'redis.md'\n  - Events: 'events.md'\n  - Background Tasks: 'background_tasks.md'\n  - Authentications: 'authentications.md'\n  - Middlewares: 'middlewares.md'\n  - WebSocket: 'websocket.md'\n  - Single-File Application: 'single_file.md'\n  - Cross-Origin Resource Sharing (CORS): 'cors.md'\n  - Open API (Swagger): 'open_api.md'\n  - Advance Configs:\n      - User Model: 'user_model.md'\n      - Template Response: 'templates.md'\n      - Timezone: 'timezone.md'\n      - Auto-Reformatting (Ruff): 'ruff.md'\n      - Log Queries: 'log_queries.md'\n      - Admin Panel: 'admin_panel.md'\n  - Deployment: 'deployment.md'\n  - Roadmap: 'roadmap.md'\n  - Release Notes: 'release_notes.md'\n\nmarkdown_extensions:\n  - admonition\n  - pymdownx.details\n  - pymdownx.superfences\n  - pymdownx.snippets\n  - pymdownx.tabbed:\n      alternate_style: true\n  - toc:\n      permalink: \"#\""
  },
  {
    "path": "docs/pre-finetuning.jsonl",
    "content": "{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is Panther?\"}, {\"role\": \"assistant\", \"content\": \"Panther is a fast, async web framework for building APIs with Python 3.10+. It features a file-based database (PantherDB), MongoDB support, built-in authentication, permissions, OpenAPI docs, WebSocket support, background tasks, middleware, and more. Panther is designed for simplicity, speed, and developer productivity.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I install Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can install Panther using pip:\\n```bash\\npip install panther\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the recommended project structure for a Panther app?\"}, {\"role\": \"assistant\", \"content\": \"A typical Panther project structure looks like this:\\n```.\\n├── app\\n│   ├── apis.py\\n│   ├── models.py\\n│   ├── serializers.py\\n│   └── urls.py\\n└── core\\n    ├── configs.py\\n    └── urls.py\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create a CRUD API for a Book model in Panther?\"}, {\"role\": \"assistant\", \"content\": \"1. Define the model in app/models.py:\\n```python\\nfrom panther.db import Model\\n\\nclass Book(Model):\\n    name: str\\n    author: str\\n    pages_count: int\\n```\\n2. Create a serializer in app/serializers.py:\\n```python\\nfrom panther.serializer import ModelSerializer\\nfrom app.models import Book\\n\\nclass BookSerializer(ModelSerializer):\\n    class Config:\\n        model = Book\\n        fields = ['name', 'author', 'pages_count']\\n```\\n3. Define API endpoints in app/apis.py (example for function-based):\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\nfrom panther.response import Response\\nfrom app.models import Book\\nfrom app.serializers import BookSerializer\\n\\n@API(input_model=BookSerializer, methods=['POST', 'GET'])\\nasync def book_api(request: Request):\\n    if request.method == 'POST':\\n        body = request.validated_data\\n        book = await Book.insert_one(**body.model_dump())\\n        return Response(data=book)\\n    elif request.method == 'GET':\\n        books = await Book.find()\\n        return Response(data=books)\\n```\\n4. Add URLs in app/urls.py:\\n```python\\nfrom app.apis import book_api\\nurls = {\\n    'book/': book_api,\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I enable authentication in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Set the auth parameter to an authentication class or function in your API definition. Panther will use this to validate the request. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.authentications import JWTAuthentication\\n@API(auth=JWTAuthentication)\\nasync def user_api(request):\\n    ...\\n```\\nIn your configs, set:\\n```python\\nAUTHENTICATION = 'panther.authentications.JWTAuthentication'\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What does the auth attribute do in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"The auth parameter can be any async function or a class with an async __call__ method. If you set auth, Panther will use your specified authentication class or function. If you do not set auth, Panther will use the default AUTHENTICATION from your config only if the request contains an authorization header. If there is no authorization header, authentication is bypassed, request.user will be None and you must rely on permissions to check the user and their authorization.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I access the authenticated user in a Panther API?\"}, {\"role\": \"assistant\", \"content\": \"After successful authentication, the authenticated user is available as request.user in your API function or method. This will be an instance of your USER_MODEL (default: BaseUser) or None if authentication was bypassed.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What happens if authentication fails in Panther?\"}, {\"role\": \"assistant\", \"content\": \"If authentication fails, Panther automatically raises HTTP_401_UNAUTHORIZED and returns a 401 Unauthorized response to the client. You do not need to handle authentication errors manually.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I use a custom authentication class with the auth attribute?\"}, {\"role\": \"assistant\", \"content\": \"Yes, you can pass a custom authentication class or function to the auth attribute. This will override the default AUTHENTICATION from your configs for that specific API. Example:\\n```python\\n@API(auth=CustomAuthentication)\\nasync def my_api(request):\\n    ...\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What built-in authentication types does Panther support?\"}, {\"role\": \"assistant\", \"content\": \"Panther provides three built-in authentication classes, all based on JWT (JSON Web Token):\\n1. JWTAuthentication: Reads the token from the 'Authorization' header as 'Bearer <token>'.\\n2. QueryParamJWTAuthentication: Reads the token from the 'authorization' query parameter (useful for WebSockets).\\n3. CookieJWTAuthentication: Reads the token from the 'access_token' cookie.\\nYou can set the authentication class in your configs with the AUTHENTICATION variable.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I configure JWT settings in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can customize JWT behavior by setting the JWT_CONFIG variable in your configs. Example:\\n```python\\nfrom datetime import timedelta\\nJWT_CONFIG = {\\n    'key': SECRET_KEY,\\n    'algorithm': 'HS256',\\n    'life_time': timedelta(days=2),\\n    'refresh_life_time': timedelta(days=10),\\n}\\n```\\nThis controls the secret key, algorithm, access token lifetime, and refresh token lifetime.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I implement custom authentication in Panther?\"}, {\"role\": \"assistant\", \"content\": \"To implement custom authentication, create a class inheriting from panther.authentications.BaseAuthentication and implement the async __call__ method. Return a user instance or raise AuthenticationAPIError on failure. Example:\\n```python\\nfrom panther.authentications import BaseAuthentication\\nclass CustomAuthentication(BaseAuthentication):\\n    async def __call__(self, request):\\n        # Your logic here\\n        return user\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What happens if I don't set auth in a Panther API?\"}, {\"role\": \"assistant\", \"content\": \"If you don't set auth, Panther will use the default AUTHENTICATION from your configs only if the request contains an authorization header. If there is no authorization header, authentication is bypassed, request.user will be None, and you must rely on permissions to check user authorization.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I define a model in Panther?\"}, {\"role\": \"assistant\", \"content\": \"To define a model, inherit from panther.db.Model and use type hints for fields. Example:\\n```python\\nfrom panther.db import Model\\nclass User(Model):\\n    username: str\\n    age: int\\n    is_active: bool\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What field types does Panther support in models?\"}, {\"role\": \"assistant\", \"content\": \"Panther supports various field types including:\\n- Basic types: str, int, float, bool\\n- Complex types: list, dict, tuple\\n- Date/time: datetime, date, time\\n- Optional fields: type | None\\n\\nExample:\\n```python\\nfrom panther.db import Model\\nfrom datetime import datetime\\nclass Product(Model):\\n    name: str\\n    price: float\\n    is_available: bool\\n    created_at: datetime\\n    tags: list[str]\\n    description: str | str\\n    metadata: dict\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create nested models in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use Pydantic BaseModel for nested fields or another Panther model. Example:\\n```python\\nfrom panther.db import Model\\nfrom pydantic import BaseModel\\n\\nclass Address(BaseModel):\\n    street: str\\n    city: str\\n    zipcode: str\\n    country: str\\n\\nclass User(Model):\\n    username: str\\n    email: str\\n    address: Address  # Nested model\\n    phone_numbers: list[str]\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle foreign keys in Panther models?\"}, {\"role\": \"assistant\", \"content\": \"Use another Panther model as a field type for foreign keys. Panther will handle the relationship. Example:\\n```python\\nfrom panther.db import Model\\n\\nclass Department(Model):\\n    name: str\\n    description: str\\n\\nclass Employee(Model):\\n    name: str\\n    email: str\\n    department: Department  # Foreign key relationship\\n    manager: Employee  # Self-referencing foreign key\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create one-to-many relationships in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use a list of the related model for one-to-many relationships. Example:\\n```python\\nfrom panther.db import Model\\n\\nclass Author(Model):\\n    name: str\\n    email: str\\n    books: list['Book']  # One author can have many books\\n\\nclass Book(Model):\\n    title: str\\n    author: Author  # Many books can belong to one author\\n    isbn: str\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create many-to-many relationships in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use lists on both sides of the relationship for many-to-many. Example:\\n```python\\nfrom panther.db import Model\\n\\nclass Student(Model):\\n    name: str\\n    courses: list['Course']  # Many students can take many courses\\n\\nclass Course(Model):\\n    name: str\\n    students: list[Student]  # Many courses can have many students\\n    instructor: str\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I add validation to model fields in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use Pydantic Field with validation rules. Example:\\n```python\\nfrom panther.db import Model\\nfrom pydantic import Field, EmailStr\\n\\nclass User(Model):\\n    username: str = Field(min_length=3, max_length=50, regex=r'^[a-zA-Z0-9_]+$')\\n    email: EmailStr\\n    age: int = Field(ge=0, le=150)\\n    password: str = Field(min_length=8)\\n    bio: str | None = Field(max_length=500, default=None)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I set default values in Panther models?\"}, {\"role\": \"assistant\", \"content\": \"Use default values in field definitions or Pydantic Field. Example:\\n```python\\nfrom panther.db import Model\\nfrom panther.utils import timezone_now\\nfrom pydantic import Field\\nfrom datetime import datetime\\n\\nclass Post(Model):\\n    title: str\\n    content: str\\n    is_published: bool = False  # Default value\\n    created_at: datetime = Field(default_factory=timezone_now)\\n    tags: list[str] = Field(default_factory=list)\\n    views: int = 0\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create computed fields in Panther models?\"}, {\"role\": \"assistant\", \"content\": \"Use Pydantic computed fields with @property or @field_validator. Example:\\n```python\\nfrom panther.db import Model\\nfrom pydantic import computed_field, field_validator\\nfrom datetime import datetime\\n\\nclass Order(Model):\\n    items: list[dict]\\n    tax_rate: float = 0.1\\n    \\n    @computed_field\\n    def total_amount(self) -> float:\\n        subtotal = sum(item['price'] * item['quantity'] for item in self.items)\\n        return subtotal * (1 + self.tax_rate)\\n    \\n    @field_validator('items')\\n    def validate_items(cls, v):\\n        if not v:\\n            raise ValueError('Order must have at least one item')\\n        return v\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle model inheritance in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can inherit from other Panther models to create base classes. Example:\\n```python\\nfrom panther.db import Model\\nfrom panther.utils import timezone_now\\nfrom datetime import datetime\\n\\nclass BaseModel(Model):\\n    created_at: datetime = Field(default_factory=timezone_now)\\n    updated_at: datetime = Field(default_factory=timezone_now)\\n    is_active: bool = True\\n\\nclass User(BaseModel):\\n    username: str\\n    email: str\\n\\nclass Product(BaseModel):\\n    name: str\\n    price: float\\n    category: str\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create complex nested structures in Panther models?\"}, {\"role\": \"assistant\", \"content\": \"Combine nested models, lists, and dictionaries for complex structures. Example:\\n```python\\nfrom panther.db import Model\\nfrom pydantic import BaseModel\\n\\nclass Address(BaseModel):\\n    street: str\\n    city: str\\n    country: str\\n\\nclass ContactInfo(BaseModel):\\n    phone: str\\n    email: str\\n    website: str | None\\n\\nclass Company(Model):\\n    name: str\\n    address: Address\\n    contacts: ContactInfo\\n    departments: list[dict[str, str]]\\n    employees: list['Employee']\\n    metadata: dict[str, any]\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle model serialization in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Panther models are automatically serializable. Use model_dump() for dictionaries and model_dump_json() for JSON. Example:\\n```python\\nfrom panther.db import Model\\n\\nclass User(Model):\\n    username: str\\n    email: str\\n    profile: dict\\n\\n# Serialization\\nuser = User(username='john', email='john@example.com', profile={'age': 25})\\nuser_dict = user.model_dump()\\nuser_json = user.model_dump_json()\\n\\n# Deserialization\\nuser_from_dict = User(**user_dict)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create model methods in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Add methods to your model classes for business logic. Example:\\n```python\\nfrom panther.db import Model\\nfrom datetime import datetime, timedelta\\n\\nclass User(Model):\\n    username: str\\n    email: str\\n    last_login: datetime\\n    is_active: bool = True\\n    \\n    def is_recently_active(self, days: int = 7) -> bool:\\n        return self.last_login > datetime.now() - timedelta(days=days)\\n    \\n    def get_display_name(self) -> str:\\n        return f'{self.username} ({self.email})'\\n    \\n    async def update_last_login(self):\\n        self.last_login = datetime.now()\\n        await self.save()\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle model validation errors in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use try-catch blocks to handle validation errors. Example:\\n```python\\nfrom panther.db import Model\\nfrom pydantic import ValidationError\\n\\nclass User(Model):\\n    username: str = Field(min_length=3)\\n    email: str = Field(regex=r'^[^@]+@[^@]+\\\\.[^@]+$')\\n    age: int = Field(ge=0, le=150)\\n\\n# Handle validation\\nasync def create_user(user_data: dict):\\n    try:\\n        user = User(**user_data)\\n        await user.save()\\n        return user\\n    except ValidationError as e:\\n        print(f'Validation error: {e.errors()}')\\n        raise\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the best practice for designing models in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use descriptive field names. Keep models focused and single-purpose. Use proper type hints. Handle validation with Pydantic Field. Use forward references for circular dependencies. Create base models for common fields. Keep nested structures manageable. Document complex relationships. Use computed fields for derived data. Handle errors gracefully.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use serializers in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Serializers transform data between your models and API requests/responses. Inherit from ModelSerializer and set the Config.model and fields. Example:\\n```python\\nfrom panther.serializer import ModelSerializer\\nfrom app.models import User\\nclass UserSerializer(ModelSerializer):\\n    class Config:\\n        model = User\\n        fields = ['username', 'age', 'is_active']\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create a WebSocket endpoint in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Inherit from GenericWebsocket and implement connect and receive methods. Register the class in your urls. Example:\\n```python\\nfrom panther.websocket import GenericWebsocket\\nclass EchoWebsocket(GenericWebsocket):\\n    async def connect(self):\\n        await self.accept()\\n    async def receive(self, data):\\n        await self.send(data)\\nurls = {\\n    'ws/echo/': EchoWebsocket,\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I implement custom permissions in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Create a class inheriting from panther.permissions.BasePermission and implement async classmethod authorization. Add your permission to the permissions list in your API. Example:\\n```python\\nfrom panther.permissions import BasePermission\\nclass CustomPermission(BasePermission):\\n    @classmethod\\n    async def authorization(cls, request):\\n        return request.user.is_admin\\nfrom panther.app import API\\n@API(permissions=[CustomPermission])\\nasync def admin_api(request):\\n    ...\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I document my Panther APIs with OpenAPI?\"}, {\"role\": \"assistant\", \"content\": \"Use the output_schema attribute with panther.openapi.OutputSchema in your API. Example:\\n```python\\nfrom panther import status\\nfrom panther.app import API\\nfrom panther.openapi import OutputSchema\\nfrom app.serializers import UserSerializer\\n@API(output_schema=OutputSchema(model=UserSerializer, status_code=status.HTTP_200_OK))\\nasync def user_api():\\n    ...\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use middleware in Panther?\"}, {\"role\": \"assistant\", \"content\": \"To use global middleware, add them to the MIDDLEWARES list in your configs. For per-API middleware, set the middlewares attribute on your API class or use the middlewares argument in the @API decorator.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use background tasks in Panther?\"}, {\"role\": \"assistant\", \"content\": \"1. Set BACKGROUND_TASKS = True in your configs.\\n2. Use BackgroundTask to schedule tasks:\\n```python\\nfrom panther.background_tasks import BackgroundTask\\ndef do_something(name):\\n    print(f'Hello {name}')\\nBackgroundTask(do_something, name='Ali').submit()\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does caching work in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Caching in Panther allows you to store API responses for GET requests for a specified duration. Set the cache attribute to a timedelta value on your API. Panther will cache the response based on user or IP, request path, query parameters, and validated data.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I enable caching in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Set the cache attribute to a timedelta value on your API. Panther will cache GET responses for the specified duration. Example:\\n```python\\nfrom datetime import timedelta\\nfrom panther.app import API\\n@API(cache=timedelta(minutes=10))\\nasync def my_api():\\n    ...\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How is the cache key built in Panther?\"}, {\"role\": \"assistant\", \"content\": \"The cache key in Panther is built from the user ID or IP, request path, a hash of the query parameters, and validated data. This ensures that cached responses are unique per user (or IP) and request parameters.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I cache non-GET requests in Panther?\"}, {\"role\": \"assistant\", \"content\": \"No, Panther only caches GET requests. POST, PUT, PATCH, and DELETE requests are not cached as they modify data and should always return fresh responses.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I clear the cache in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Panther automatically manages cache expiration based on the timedelta you set. You can also manually clear the cache by restarting your application or using Panther's cache management utilities if available.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the best practice for using cache in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use caching for GET endpoints that return data that doesn't change frequently to improve performance. Set appropriate cache durations based on how often your data changes. Be careful not to cache sensitive or user-specific data inappropriately.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does caching work with authentication in Panther?\"}, {\"role\": \"assistant\", \"content\": \"When caching is enabled, Panther includes the user ID in the cache key if the user is authenticated, or the IP address if not authenticated. This ensures that cached responses are user-specific and secure.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I disable caching for specific APIs in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Yes, you can disable caching for specific APIs by setting cache=None or not setting the cache attribute at all. This will override any global cache settings for that specific API.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does caching affect API performance in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Caching significantly improves API performance by serving cached responses instead of executing the full request logic. This reduces database queries, computation time, and response latency, especially for frequently accessed data.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does throttling work in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Throttling in Panther limits the number of requests a user or IP can make in a given time period. Set the throttling attribute on your API to an instance of panther.throttling.Throttle. If the rate is exceeded, Panther returns a 429 Too Many Requests error.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I enable throttling in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Set the throttling attribute on your API to an instance of panther.throttling.Throttle. Example:\\n```python\\nfrom datetime import timedelta\\nfrom panther.app import API\\nfrom panther.throttling import Throttle\\n@API(throttling=Throttle(rate=5, duration=timedelta(minutes=1)))\\nasync def my_api():\\n    ...\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I set global throttling in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Set the THROTTLING variable in your configs to a Throttle instance. This will apply to all APIs unless overridden by a per-API throttling setting. Example:\\n```python\\nfrom datetime import timedelta\\nfrom panther.throttling import Throttle\\nTHROTTLING = Throttle(rate=10, duration=timedelta(minutes=1))\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does Panther handle requests that exceed the throttle limit?\"}, {\"role\": \"assistant\", \"content\": \"If a user exceeds the allowed number of requests within the throttle duration, Panther returns a 429 Too Many Requests response and bans the user for the duration.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I customize how throttling keys are built in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Yes, you can customize throttling behavior by subclassing panther.throttling.Throttle and overriding the build_cache_key method. Example:\\n```python\\nfrom panther.throttling import Throttle\\nclass CustomThrottle(Throttle):\\n    def build_cache_key(self, request):\\n        # Custom logic here\\n        return ...\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the best practice for using throttling in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use throttling to protect your APIs from abuse. Set appropriate rates and durations for your use case. Consider using different throttling rules for different types of endpoints (e.g., stricter limits for authentication endpoints).\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do throttling and caching work together in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Throttling and caching are independent features. Throttling checks happen before caching, so even if a response is cached, the request still counts toward the throttle limit. This prevents abuse even for cached endpoints.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I use different throttling rules for different HTTP methods?\"}, {\"role\": \"assistant\", \"content\": \"Yes, you can set different throttling rules for different HTTP methods by creating separate API endpoints or using custom throttling logic in your build_cache_key method to include the HTTP method in the cache key.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does throttling work with authentication in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Throttling can be based on user ID (if authenticated) or IP address (if not authenticated). When a user is authenticated, throttling is typically per-user. When not authenticated, it's per-IP address to prevent abuse.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What happens if I don't set any throttling in Panther?\"}, {\"role\": \"assistant\", \"content\": \"If you don't set any throttling (neither global nor per-API), there are no rate limits on your APIs. This means users can make unlimited requests, which could lead to abuse or performance issues in production.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I enable and use the admin panel in Panther?\"}, {\"role\": \"assistant\", \"content\": \"1. Add the admin panel URLs in your main URL config:\\n```python\\nfrom panther.panel.urls import url_routing as panel_url_routing\\nurl_routing = {\\n    'panel/': panel_url_routing,\\n}\\n```\\n2. Start your app and visit http://127.0.0.1:8000/panel/ in your browser.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does the CORS middleware work in Panther?\"}, {\"role\": \"assistant\", \"content\": \"The CORS middleware in Panther automatically adds Cross-Origin Resource Sharing (CORS) headers to HTTP responses based on your configuration. It handles preflight (OPTIONS) requests and sets headers like Access-Control-Allow-Origin, Access-Control-Allow-Methods, and others.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I enable CORS in Panther?\"}, {\"role\": \"assistant\", \"content\": \"1. Set CORS config variables in your configs, e.g. ALLOW_ORIGINS, ALLOW_METHODS, etc.\\n2. Add 'panther.middlewares.cors.CORSMiddleware' to your MIDDLEWARES list.\\nExample:\\n```python\\nALLOW_ORIGINS = ['*']\\nMIDDLEWARES = [\\n    'panther.middlewares.cors.CORSMiddleware',\\n]\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What CORS configuration options does Panther support?\"}, {\"role\": \"assistant\", \"content\": \"Panther supports several CORS configuration options:\\n- ALLOW_ORIGINS: List of allowed origins (e.g., ['https://example.com'])\\n- ALLOW_METHODS: List of allowed HTTP methods (e.g., ['GET', 'POST'])\\n- ALLOW_HEADERS: List of allowed headers (e.g., ['Content-Type', 'Authorization'])\\n- ALLOW_CREDENTIALS: Boolean for allowing credentials\\n- EXPOSE_HEADERS: List of headers to expose\\n- CORS_MAX_AGE: Integer for preflight cache duration\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I configure specific CORS settings in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Set the CORS configuration variables in your configs. Example:\\n```python\\nALLOW_ORIGINS = ['https://example.com', 'https://app.example.com']\\nALLOW_METHODS = ['GET', 'POST', 'PUT', 'DELETE']\\nALLOW_HEADERS = ['Content-Type', 'Authorization', 'X-Custom-Header']\\nALLOW_CREDENTIALS = True\\nEXPOSE_HEADERS = ['X-Custom-Header']\\nCORS_MAX_AGE = 3600\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the difference between ALLOW_ORIGINS=['*'] and specific origins?\"}, {\"role\": \"assistant\", \"content\": \"ALLOW_ORIGINS=['*'] allows requests from any origin, which is convenient for development but not recommended for production. Specific origins (e.g., ['https://example.com']) are more secure as they only allow requests from trusted domains.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle preflight requests with CORS in Panther?\"}, {\"role\": \"assistant\", \"content\": \"The CORS middleware automatically handles preflight (OPTIONS) requests. When a browser sends a preflight request, the middleware responds with the appropriate CORS headers based on your configuration. You don't need to handle OPTIONS requests manually.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I use CORS with credentials in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Yes, you can enable credentials by setting ALLOW_CREDENTIALS = True in your configs. However, when using credentials, you cannot use ALLOW_ORIGINS = ['*'] - you must specify specific origins for security reasons.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the best practice for CORS configuration in Panther?\"}, {\"role\": \"assistant\", \"content\": \"For development, you can use ALLOW_ORIGINS = ['*'] for convenience. For production, always specify exact origins, enable only necessary methods and headers, and set appropriate CORS_MAX_AGE. Consider security implications when allowing credentials.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I debug CORS issues in Panther?\"}, {\"role\": \"assistant\", \"content\": \"To debug CORS issues, check your browser's developer console for CORS error messages. Verify that your ALLOW_ORIGINS includes the correct origin, that ALLOW_METHODS includes the HTTP method being used, and that the CORS middleware is properly added to your MIDDLEWARES list.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I apply CORS only to specific APIs in Panther?\"}, {\"role\": \"assistant\", \"content\": \"The CORS middleware applies globally when added to MIDDLEWARES. If you need different CORS settings for different APIs, you would need to create custom middleware or handle CORS headers manually in those specific endpoints.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I log database query performance in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Set 'LOG_QUERIES = True' in your configs. Panther will log query execution times to 'logs/query.log'. Example:\\n```python\\nLOG_QUERIES = True\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use environment variables in Panther configs?\"}, {\"role\": \"assistant\", \"content\": \"You can use Panther's 'load_env' utility to load variables from a .env file. Example:\\n```python\\nfrom panther.utils import load_env\\nfrom pathlib import Path\\nBASE_DIR = Path(__name__).resolve().parent\\nenv = load_env(BASE_DIR / '.env')\\nSECRET_KEY = env['SECRET_KEY']\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I define a model with nested fields or foreign keys in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can use pydantic.BaseModel for nested fields or another Panther model for foreign keys. Example:\\n```python\\nfrom panther.db import Model\\nfrom pydantic import BaseModel\\nclass Address(BaseModel):\\n    city: str\\n    zipcode: str\\nclass User(Model):\\n    username: str\\n    address: Address\\nclass Department(Model):\\n    name: str\\nclass Employee(Model):\\n    name: str\\n    department: Department\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What OpenAPI UI types does Panther support?\"}, {\"role\": \"assistant\", \"content\": \"Panther supports several OpenAPI UI types out of the box. You can use ScalarOpenAPI, SwaggerOpenAPI, RedocOpenAPI, RapiDocOpenAPI, and SpotlightOpenAPI. Import them from panther.openapi.views and register them in your URL routing. Example:\\n```python\\nfrom panther.openapi.views import ScalarOpenAPI, SwaggerOpenAPI, RedocOpenAPI\\nurl_routing = {\\n    'docs/scalar/': ScalarOpenAPI,\\n    'docs/swagger/': SwaggerOpenAPI,\\n    'docs/redoc/': RedocOpenAPI,\\n}\\n```\\nYou can also use the bundled openapi_url_routing to include all UI types at once:\\n```python\\nfrom panther.openapi.urls import url_routing as openapi_url_routing\\nurl_routing = {\\n    'docs/': openapi_url_routing,\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does Panther determine response models and status codes for OpenAPI docs?\"}, {\"role\": \"assistant\", \"content\": \"Panther follows a specific logic to generate OpenAPI documentation:\\n1. output_schema: Panther first looks for an output_schema attribute to generate the OpenAPI documentation. This is the recommended and most accurate way.\\n2. output_model: If output_schema does not exist, Panther looks for an output_model attribute to generate the response type and attempts to extract the status code from your source code.\\n3. Source Code Analysis: If neither is available, Panther tries to extract the response data and status code directly from your source code using static analysis with ast.\\nFor best results, always specify output_schema in your APIs.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I add pagination, filtering, and sorting to a Panther List API?\"}, {\"role\": \"assistant\", \"content\": \"Inherit from ListAPI and set the pagination, search_fields, filter_fields, and sort_fields attributes. Example:\\n```python\\nfrom panther.generics import ListAPI\\nfrom panther.pagination import Pagination\\nclass BookAPI(ListAPI):\\n    pagination = Pagination\\n    search_fields = ['name', 'author']\\n    filter_fields = ['name', 'author']\\n    sort_fields = ['name', 'pages_count']\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does Panther handle error responses?\"}, {\"role\": \"assistant\", \"content\": \"Panther raises exceptions like AuthenticationAPIError, AuthorizationAPIError, and NotFoundAPIError for common error cases. You can raise these in your code to return standard error responses. Example:\\n```python\\nfrom panther.exceptions import NotFoundAPIError\\nif not user:\\n    raise NotFoundAPIError('User not found')\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I deploy a Panther app?\"}, {\"role\": \"assistant\", \"content\": \"You can deploy Panther using any ASGI server such as Uvicorn, Granian, or Daphne. For production, use Gunicorn with Uvicorn workers:\\n```bash\\ngunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app\\n```\\nMake sure to set up environment variables and production configs as needed.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use OpenAPI output_schema for best documentation?\"}, {\"role\": \"assistant\", \"content\": \"Always set the output_schema attribute on your APIs using panther.openapi.OutputSchema. This ensures Panther generates accurate OpenAPI documentation for your endpoints, including the correct response model and status code.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle custom error responses in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Raise panther.exceptions.APIError or create your own exception class inheriting from APIError. Example:\\n```python\\nfrom panther.exceptions import APIError\\nraise APIError('Custom error message', status_code=418)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What does the input_model attribute do in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"The input_model attribute specifies a serializer class (Pydantic or ModelSerializer) that validates and parses incoming request data. For POST, PUT, and PATCH requests, Panther validates request.data using this serializer and provides the validated data as request.validated_data. Example:\\n```python\\nfrom panther.app import API\\nfrom app.serializers import BookSerializer\\n@API(input_model=BookSerializer)\\nasync def create_book(request):\\n    data = request.validated_data\\n    ...\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does Panther validate input data with input_model?\"}, {\"role\": \"assistant\", \"content\": \"When you set input_model, Panther uses the specified serializer to validate incoming data for POST, PUT, and PATCH requests. If the data is invalid, Panther returns a 400 Bad Request with details about the validation errors. If valid, the parsed data is available as request.validated_data.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What types of serializers can I use for input_model in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can use either a Pydantic model or a Panther ModelSerializer for input_model. ModelSerializer is recommended for models that map directly to your database, while Pydantic models are useful for custom validation or non-database data.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I use input_model for GET requests?\"}, {\"role\": \"assistant\", \"content\": \"No, input_model is only used for validating data in POST, PUT, and PATCH requests. For GET requests, Panther does not validate request.data using input_model.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What happens if input data does not match the input_model in Panther?\"}, {\"role\": \"assistant\", \"content\": \"If the incoming data does not match the input_model's schema or fails validation, Panther automatically returns a 400 Bad Request response with details about the validation errors. You do not need to handle this manually.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I access validated input data in a Panther API?\"}, {\"role\": \"assistant\", \"content\": \"After Panther validates the request data using input_model, the validated and parsed data is available as request.validated_data in your API function or method.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What does the output_schema attribute do in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"The output_schema attribute is used for OpenAPI documentation. It should be an instance of panther.openapi.OutputSchema, specifying both the response model and status code. This helps Panther generate accurate OpenAPI docs. Example:\\n```python\\nfrom panther import status\\nfrom panther.app import API\\nfrom panther.openapi import OutputSchema\\nfrom app.serializers import BookSerializer\\n@API(output_schema=OutputSchema(model=BookSerializer, status_code=status.HTTP_201_CREATED))\\nasync def create_book():\\n    ...\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does output_schema improve OpenAPI documentation in Panther?\"}, {\"role\": \"assistant\", \"content\": \"By setting output_schema, you explicitly define the response model and status code for your API in the OpenAPI docs. This ensures the generated documentation is accurate and matches your API's actual behavior.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the difference between output_model and output_schema in Panther?\"}, {\"role\": \"assistant\", \"content\": \"output_model only specifies the serializer for the response data, while output_schema specifies both the serializer (model) and the HTTP status code for OpenAPI documentation. For best documentation, use output_schema.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Is it required to use output_schema in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"No, output_schema is optional, but it is recommended for APIs where you want precise OpenAPI documentation, especially for public APIs or when you need to specify custom status codes.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I use output_schema to document multiple response types in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Currently, output_schema documents a single response model and status code per API. For more complex cases (e.g., multiple possible responses), you may need to extend the OpenAPI docs manually.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What happens if I set both output_model and output_schema in Panther?\"}, {\"role\": \"assistant\", \"content\": \"If both are set, Panther uses output_schema for OpenAPI documentation and output_model for response serialization. However, for best results, keep them consistent (use the same serializer/model in both).\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What does the output_model attribute do in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"The output_model attribute specifies a serializer class to automatically serialize your API response data. When set, Panther uses this serializer to format the response, ensuring a consistent and validated output structure. Example:\\n```python\\nfrom panther.app import API\\nfrom app.serializers import BookSerializer\\n@API(output_model=BookSerializer)\\nasync def get_book():\\n    ...\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does Panther use output_model to serialize responses?\"}, {\"role\": \"assistant\", \"content\": \"When output_model is set, Panther serializes the data returned by your API using the specified serializer before sending it in the HTTP response. This ensures the response matches the expected schema and is validated.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What types of serializers can I use for output_model in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can use either a Pydantic model or a Panther ModelSerializer for output_model. ModelSerializer is recommended for serializing database models, while Pydantic models are useful for custom or computed responses.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does output_model affect OpenAPI documentation in Panther?\"}, {\"role\": \"assistant\", \"content\": \"When you set output_model, Panther uses it to generate the response schema in the OpenAPI documentation. For best results, use output_schema for full control over both the model and status code.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Is it required to use output_model in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"No, output_model is optional. However, using it is recommended for consistent, validated, and well-documented API responses, especially for public APIs or when using OpenAPI docs.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What happens if the response data does not match the output_model in Panther?\"}, {\"role\": \"assistant\", \"content\": \"If the response data does not match the output_model's schema, Panther will raise a validation error and return a 500 Internal Server Error. Always ensure your API returns data compatible with the output_model.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do permissions work in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Permissions in Panther allow you to control access to APIs and WebSockets based on custom logic. You define permission classes that implement an async __call__ method. Panther checks each permission before allowing access. If any permission returns False, a 403 Forbidden error is raised.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I define a custom permission class in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Create a class that inherits from panther.permissions.BasePermission and implement the async __call__ method. This method receives the request (or WebSocket) and should return True or False. Example:\\n```python\\nfrom panther.permissions import BasePermission\\nclass IsAdmin(BasePermission):\\n    async def __call__(self, request):\\n        return getattr(request.user, 'is_admin', False)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use permissions in a Panther API?\"}, {\"role\": \"assistant\", \"content\": \"Set the permissions attribute (class-based) or argument (function-based) to a list of permission classes. Panther will check all listed permissions before allowing access. Example:\\n```python\\nfrom panther.app import API\\n@API(permissions=[IsAdmin])\\nasync def admin_api(request):\\n    ...\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use permissions in Panther WebSockets?\"}, {\"role\": \"assistant\", \"content\": \"Set the permissions attribute on your GenericWebsocket subclass to a list of permission classes. Panther will check these before accepting the connection. Example:\\n```python\\nclass MyWebSocket(GenericWebsocket):\\n    permissions = [IsAdmin]\\n    async def connect(self):\\n        await self.accept()\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What should the permission __call__ method return in Panther?\"}, {\"role\": \"assistant\", \"content\": \"The __call__ method should return True if access is allowed, or False to deny access. If any permission returns False, Panther raises a 403 Forbidden error.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I use multiple permission classes in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Yes, you can provide a list of permission classes. Panther will call the __call__ method of each class. All must return True for access to be granted.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What happens if a permission check fails in Panther?\"}, {\"role\": \"assistant\", \"content\": \"If any permission's __call__ method returns False, Panther raises a panther.exceptions.AuthorizationAPIError and returns a 403 Forbidden response to the client.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the best practice for writing permission classes in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Keep your permission logic simple and focused. Use one class per rule, and combine them as needed in your APIs. Always make the __call__ method async and return True or False.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I use async functions as permissions in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Yes, you can use any async function or class with an async __call__ method as a permission. Panther will call each permission asynchronously. Example:\\n```python\\nasync def custom_permission(request):\\n    return request.user.is_active\\n@API(permissions=[custom_permission])\\nasync def my_api(request):\\n    ...\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do permissions interact with authentication in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Permissions are checked after authentication. If authentication is bypassed (no auth set or no authorization header), request.user will be None, and you must handle this case in your permission logic. Always check if request.user exists before accessing user attributes.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do middlewares work in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Middlewares in Panther are functions or classes that process requests and responses. They run in sequence before and after your API endpoints. You can use global middlewares (applied to all APIs) or per-API middlewares (applied to specific endpoints).\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use global middlewares in Panther?\"}, {\"role\": \"assistant\", \"content\": \"To use global middlewares, add them to the MIDDLEWARES list in your configs. These middlewares will run for all APIs in your application. Example:\\n```python\\nMIDDLEWARES = [\\n    'core.middlewares.MyMiddleware',\\n    'panther.middlewares.cors.CORSMiddleware',\\n]\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use per-API middlewares in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can assign middlewares to specific APIs using the middlewares attribute (class-based) or argument (function-based). These middlewares run only for the specified API, after global middlewares. Example:\\n```python\\nfrom panther.app import API\\n@API(middlewares=[CustomMiddleware])\\nasync def my_api(request):\\n    ...\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create a custom middleware in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Create a class that inherits from panther.middlewares.HTTPMiddleware and implement the __call__ method. This method should process the request and call self.dispatch to continue the middleware chain. Example:\\n```python\\nfrom panther.middlewares import HTTPMiddleware\\nclass CustomMiddleware(HTTPMiddleware):\\n    async def __call__(self, request):\\n        # Process request\\n        response = await self.dispatch(request=request)\\n        # Process response\\n        return response\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the middleware execution order in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Middlewares execute in the order they are defined. Global middlewares run first, followed by per-API middlewares. Within each group, middlewares execute in the order they appear in the list. The response flows back through the middlewares in reverse order.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What built-in middlewares does Panther provide?\"}, {\"role\": \"assistant\", \"content\": \"Panther provides several built-in middlewares including CORSMiddleware for handling Cross-Origin Resource Sharing, and other utility middlewares. You can import them from panther.middlewares and use them in your MIDDLEWARES list or per-API.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I use async functions as middlewares in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Yes, you can use async functions as middlewares. The function should accept a request parameter and return a response. Example:\\n```python\\nasync def custom_middleware(request):\\n    # Process request\\n    response = await dispatch_request(request)\\n    # Process response\\n    return response\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle errors in Panther middlewares?\"}, {\"role\": \"assistant\", \"content\": \"You can catch exceptions in your middleware and handle them appropriately. If you want to stop the middleware chain, you can return a response directly. Otherwise, let the exception propagate to be handled by Panther's error handling system.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the best practice for writing middlewares in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Keep middlewares focused on a single responsibility. Use global middlewares for cross-cutting concerns like logging, CORS, or authentication. Use per-API middlewares for specific endpoint requirements. Always call self.dispatch to continue the middleware chain unless you want to short-circuit the request.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do middlewares interact with authentication and permissions in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Middlewares run before authentication and permissions checks. This means you can use middlewares to modify the request before authentication, or to add custom headers that might be needed for authentication. However, user information (request.user) is not available in middlewares that run before authentication.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What types of APIs does Panther support?\"}, {\"role\": \"assistant\", \"content\": \"Panther supports several types of APIs:\\n1. Function-based APIs using the @API decorator\\n2. Class-based APIs inheriting from GenericAPI\\n3. Generic APIs (ListAPI, CreateAPI, etc.) for common CRUD operations\\n4. Each type has different use cases and capabilities.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create a function-based API in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use the @API decorator on an async function. You can specify methods, input_model, output_model, auth, permissions, and other attributes. Example:\\n```python\\nfrom panther.app import API\\n@API(methods=['GET', 'POST'])\\nasync def my_api(request):\\n    if request.method == 'GET':\\n        return {'data': 'Hello'}\\n    elif request.method == 'POST':\\n        return {'data': 'Created'}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create a class-based API in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Inherit from GenericAPI and implement methods like get, post, put, patch, delete. Each method corresponds to an HTTP method. Example:\\n```python\\nfrom panther.app import GenericAPI\\nclass UserAPI(GenericAPI):\\n    async def get(self, request):\\n        return {'users': []}\\n    async def post(self, request):\\n        return {'message': 'User created'}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the difference between function-based and class-based APIs in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Function-based APIs use a single function with request.method checks, while class-based APIs use separate methods (get, post, etc.) for each HTTP method. Class-based APIs are more organized and easier to maintain for complex endpoints with multiple HTTP methods.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use ListAPI in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Inherit from ListAPI to create an API that handles GET requests with pagination, filtering, and sorting. Set attributes like pagination, search_fields, filter_fields, and sort_fields. Example:\\n```python\\nfrom panther.generics import ListAPI\\nfrom panther.pagination import Pagination\\nclass BookListAPI(ListAPI):\\n    pagination = Pagination\\n    search_fields = ['name', 'author']\\n    filter_fields = ['name', 'author']\\n    sort_fields = ['name', 'pages_count']\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What are Generic APIs in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Generic APIs are pre-built classes that provide common CRUD functionality. They include ListAPI (for listing with pagination), CreateAPI (for creating), RetrieveAPI (for single item retrieval), UpdateAPI (for updating), and DeleteAPI (for deleting). These reduce boilerplate code.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"When should I use function-based vs class-based APIs in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use function-based APIs for simple endpoints with one or two HTTP methods. Use class-based APIs for complex endpoints with multiple HTTP methods or when you need better organization. Use Generic APIs when you need standard CRUD operations with built-in features like pagination and filtering.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I mix different API types in the same project?\"}, {\"role\": \"assistant\", \"content\": \"Yes, you can mix function-based, class-based, and Generic APIs in the same project. Each API type is independent, and you can choose the most appropriate type for each endpoint based on your requirements.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle different HTTP methods in function-based APIs?\"}, {\"role\": \"assistant\", \"content\": \"Use request.method to check the HTTP method and handle each case. You can also use the methods parameter in the @API decorator to restrict which methods are allowed. Example:\\n```python\\n@API(methods=['GET', 'POST'])\\nasync def my_api(request):\\n    match request.method:\\n        case 'GET':\\n            return {'data': 'get'}\\n        case 'POST':\\n            return {'data': 'post'}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What are the advantages of using Generic APIs in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Generic APIs provide built-in functionality like pagination, filtering, searching, and sorting without writing boilerplate code. They follow REST conventions and provide consistent behavior across your application. They're especially useful for standard CRUD operations.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do WebSockets work in Panther?\"}, {\"role\": \"assistant\", \"content\": \"WebSockets in Panther provide real-time bidirectional communication between clients and servers. They allow persistent connections where both client and server can send messages at any time. Panther's WebSocket implementation is built on top of ASGI and supports authentication, permissions, and custom message handling.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create a WebSocket endpoint in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Inherit from GenericWebsocket and implement the required methods. Register the class in your urls. Example:\\n```python\\nfrom panther.websocket import GenericWebsocket\\nclass EchoWebsocket(GenericWebsocket):\\n    async def connect(self):\\n        await self.accept()\\n    async def receive(self, data):\\n        await self.send(data)\\nurls = {\\n    'ws/echo/': EchoWebsocket,\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What methods can I implement in a Panther WebSocket?\"}, {\"role\": \"assistant\", \"content\": \"You can implement these methods in your WebSocket class:\\n- connect(): Called when a client connects\\n- disconnect(): Called when a client disconnects\\n- receive(data): Called when a message is received from the client\\n- send(data): Send a message to the client\\n- close(): Close the connection\\n- accept(): Accept the WebSocket connection\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle WebSocket authentication in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Set the auth attribute on your WebSocket class to an authentication class. Panther will authenticate the connection before calling connect(). Example:\\n```python\\nfrom panther.websocket import GenericWebsocket\\nfrom panther.authentications import JWTAuthentication\\nclass ChatWebsocket(GenericWebsocket):\\n    auth = JWTAuthentication\\n    async def connect(self):\\n        # self.user is available after authentication\\n        await self.accept()\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use permissions with WebSockets in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Set the permissions attribute on your WebSocket class to a list of permission classes. Panther will check these before accepting the connection. Example:\\n```python\\nfrom panther.websocket import GenericWebsocket\\nclass AdminWebsocket(GenericWebsocket):\\n    permissions = [IsAdmin]\\n    async def connect(self):\\n        await self.accept()\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I send messages to WebSocket clients in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use the send() method to send messages to the connected client. You can send strings, dictionaries, or other serializable data. Example:\\n```python\\nasync def receive(self, data):\\n    # Echo the message back\\n    await self.send(data)\\n    # Send a custom message\\n    await self.send({'type': 'notification', 'message': 'Hello!'})\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle different message types in Panther WebSockets?\"}, {\"role\": \"assistant\", \"content\": \"You can check the message type in the receive() method and handle different types accordingly. Example:\\n```python\\nasync def receive(self, data):\\n    if isinstance(data, dict):\\n        message_type = data.get('type')\\n        if message_type == 'chat':\\n            await self.handle_chat(data)\\n        elif message_type == 'join':\\n            await self.handle_join(data)\\n    else:\\n        await self.send({'error': 'Invalid message format'})\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I broadcast messages to multiple WebSocket clients in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can maintain a list of connected clients and broadcast to all of them. Example:\\n```python\\nclass ChatWebsocket(GenericWebsocket):\\n    connected_clients = []\\n    async def connect(self):\\n        await self.accept()\\n        self.connected_clients.append(self)\\n    async def disconnect(self):\\n        self.connected_clients.remove(self)\\n    async def broadcast(self, message):\\n        for client in self.connected_clients:\\n            await client.send(message)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle WebSocket errors in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can catch exceptions in your WebSocket methods and handle them appropriately. Use try-catch blocks and send error messages to clients. Example:\\n```python\\nasync def receive(self, data):\\n    try:\\n        await self.process_message(data)\\n    except Exception as e:\\n        await self.send({'error': str(e)})\\n        await self.close()\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I close a WebSocket connection in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use the close() method to close the WebSocket connection. You can also specify a close code and reason. Example:\\n```python\\nasync def disconnect(self):\\n    # Connection is automatically closed\\n    pass\\nasync def force_disconnect(self):\\n    await self.close(code=1000, reason='User requested disconnect')\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I access WebSocket connection information in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can access connection information through the WebSocket object. Example:\\n```python\\nasync def connect(self):\\n    # Get client IP\\n    client_ip = self.scope.get('client')[0]\\n    # Get headers\\n    headers = dict(self.scope.get('headers', []))\\n    # Get query parameters\\n    query_params = self.scope.get('query_string', b'').decode()\\n    await self.accept()\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I implement a chat room with Panther WebSockets?\"}, {\"role\": \"assistant\", \"content\": \"Create a WebSocket that maintains a list of connected clients and broadcasts messages. Example:\\n```python\\nclass ChatRoom(GenericWebsocket):\\n    connected_clients = []\\n    async def connect(self):\\n        await self.accept()\\n        self.connected_clients.append(self)\\n        await self.broadcast({'type': 'join', 'user': self.user.username})\\n    async def disconnect(self):\\n        self.connected_clients.remove(self)\\n        await self.broadcast({'type': 'leave', 'user': self.user.username})\\n    async def receive(self, data):\\n        await self.broadcast({'type': 'message', 'user': self.user.username, 'message': data})\\n    async def broadcast(self, message):\\n        for client in self.connected_clients:\\n            await client.send(message)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle WebSocket reconnection in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Clients can reconnect by establishing a new WebSocket connection. You can implement reconnection logic by maintaining user sessions and allowing reconnection with the same authentication token. The connect() method will be called again for each new connection.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I implement WebSocket rate limiting in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can implement rate limiting by tracking message frequency per client and limiting the number of messages sent within a time period. Example:\\n```python\\nimport time\\nclass RateLimitedWebsocket(GenericWebsocket):\\n    message_times = {}\\n    async def receive(self, data):\\n        client_id = self.user.id\\n        current_time = time.time()\\n        if client_id in self.message_times:\\n            if current_time - self.message_times[client_id] < 1:  # 1 second limit\\n                await self.send({'error': 'Rate limit exceeded'})\\n                return\\n        self.message_times[client_id] = current_time\\n        await self.send(data)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I test WebSocket endpoints in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can test WebSocket endpoints using tools like wscat, browser WebSocket APIs, or Python libraries like websockets. Example with wscat:\\n```bash\\nwscat -c ws://localhost:8000/ws/echo/\\n```\\nOr with Python:\\n```python\\nimport asyncio\\nimport websockets\\nasync def test_websocket():\\n    async with websockets.connect('ws://localhost:8000/ws/echo/') as websocket:\\n        await websocket.send('Hello')\\n        response = await websocket.recv()\\n        print(response)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the best practice for WebSocket error handling in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Always wrap WebSocket operations in try-catch blocks. Send meaningful error messages to clients. Log errors for debugging. Handle connection drops gracefully. Use appropriate close codes when closing connections. Example:\\n```python\\nasync def receive(self, data):\\n    try:\\n        await self.process_message(data)\\n    except ValueError as e:\\n        await self.send({'error': 'Invalid data format'})\\n    except Exception as e:\\n        await self.send({'error': 'Internal server error'})\\n        await self.close(code=1011)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do WebSockets work with Panther's authentication system?\"}, {\"role\": \"assistant\", \"content\": \"WebSockets can use the same authentication classes as regular APIs. Set the auth attribute on your WebSocket class. After successful authentication, self.user will contain the authenticated user. If authentication fails, the connection will be rejected before connect() is called.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I use WebSockets with Panther's caching system?\"}, {\"role\": \"assistant\", \"content\": \"WebSockets don't use Panther's caching system since they maintain persistent connections and send real-time data. Caching is designed for HTTP requests, while WebSockets are for live, bidirectional communication.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I implement WebSocket rooms or channels in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can implement rooms by maintaining a dictionary of room members and broadcasting messages only to clients in specific rooms. Example:\\n```python\\nclass RoomWebsocket(GenericWebsocket):\\n    rooms = {}\\n    async def connect(self):\\n        await self.accept()\\n        room_id = self.scope.get('url_route', {}).get('kwargs', {}).get('room_id')\\n        if room_id not in self.rooms:\\n            self.rooms[room_id] = []\\n        self.rooms[room_id].append(self)\\n    async def receive(self, data):\\n        room_id = self.scope.get('url_route', {}).get('kwargs', {}).get('room_id')\\n        for client in self.rooms[room_id]:\\n            await client.send(data)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does routing work in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Routing in Panther maps URL patterns to API endpoints or WebSocket classes. Panther uses a simple dictionary-based routing system where keys are URL patterns and values are API functions or WebSocket classes. Routes are matched in order, and the first match is used.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I define basic routes in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Create a urls.py file and define a urls dictionary with URL patterns as keys and API functions or WebSocket classes as values. Example:\\n```python\\nfrom app.apis import user_api, book_api\\nfrom app.websockets import ChatWebsocket\\nurls = {\\n    'user/': user_api,\\n    'book/': book_api,\\n    'ws/chat/': ChatWebsocket,\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create nested routes in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can create nested routes by defining a urls dictionary within another urls dictionary. Example:\\n```python\\nfrom app.apis import user_api, user_detail_api\\nfrom app.apis import book_api, book_detail_api\\nurls = {\\n    'user/': {\\n        '': user_api,\\n        '<id>/': user_detail_api,\\n    },\\n    'book/': {\\n        '': book_api,\\n        '<id>/': book_detail_api,\\n    },\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use URL parameters in Panther routes?\"}, {\"role\": \"assistant\", \"content\": \"Use angle brackets to define URL parameters. The parameter name will be available in your API function. Example:\\n```python\\nfrom app.apis import user_detail_api\\nurls = {\\n    'user/<id>/': user_detail_api,\\n}\\n# In your API function:\\nasync def user_detail_api(request, id):\\n    # id parameter is available here\\n    return {'user_id': id}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle multiple URL parameters in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can define multiple parameters in the URL pattern. They will be passed to your API function in the order they appear. Example:\\n```python\\nfrom app.apis import book_chapter_api\\nurls = {\\n    'book/<book_id>/chapter/<chapter_id>/': book_chapter_api,\\n}\\n# In your API function:\\nasync def book_chapter_api(request, book_id, chapter_id):\\n    return {'book_id': book_id, 'chapter_id': chapter_id}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I include routes from other modules in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Import the urls dictionary from other modules and include it in your main urls dictionary. Example:\\n```python\\nfrom app.apis import user_urls, book_urls\\nfrom app.websockets import websocket_urls\\nurls = {\\n    'api/': {\\n        'user/': user_urls,\\n        'book/': book_urls,\\n    },\\n    'ws/': websocket_urls,\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I set up the main URL configuration in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Create a main.py file and import your urls dictionary. Panther will automatically use this for routing. Example:\\n```python\\nfrom panther import Panther\\nfrom app.urls import urls\\napp = Panther(__name__)\\napp.include_urls(urls)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle trailing slashes in Panther routes?\"}, {\"role\": \"assistant\", \"content\": \"Panther automatically handles trailing slashes. You can define routes with or without trailing slashes, and Panther will match both. However, it's recommended to be consistent and use trailing slashes for clarity.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create API versioning in Panther routes?\"}, {\"role\": \"assistant\", \"content\": \"You can create API versioning by organizing routes with version prefixes. Example:\\n```python\\nfrom app.apis.v1 import user_api as user_api_v1\\nfrom app.apis.v2 import user_api as user_api_v2\\nurls = {\\n    'api/v1/': {\\n        'user/': user_api_v1,\\n    },\\n    'api/v2/': {\\n        'user/': user_api_v2,\\n    },\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle 404 errors for unmatched routes in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Panther automatically returns a 404 Not Found response when no route matches the requested URL. You don't need to handle this manually. Panther will also return a 405 Method Not Allowed if the route exists but doesn't support the HTTP method.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I organize routes in a large Panther application?\"}, {\"role\": \"assistant\", \"content\": \"For large applications, organize routes by feature or module. Create separate urls.py files for each module and include them in the main urls dictionary. Example:\\n```python\\n# app/urls.py\\nfrom app.apis.user import urls as user_urls\\nfrom app.apis.book import urls as book_urls\\nfrom app.apis.order import urls as order_urls\\nurls = {\\n    'user/': user_urls,\\n    'book/': book_urls,\\n    'order/': order_urls,\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use regex patterns in Panther routes?\"}, {\"role\": \"assistant\", \"content\": \"Panther uses simple string matching for routes, not regex patterns. Use URL parameters with angle brackets for dynamic parts of the URL. If you need complex pattern matching, handle it in your API function logic.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create optional URL parameters in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Panther doesn't support optional URL parameters in the route definition. You can handle optional parameters by creating multiple routes or by using query parameters instead. Example:\\n```python\\n# Use query parameters for optional data\\nurls = {\\n    'user/<id>/': user_detail_api,\\n}\\n# In your API: request.query_params.get('include_details')\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle route conflicts in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Routes are matched in the order they are defined in the urls dictionary. More specific routes should be defined before more general ones. Panther will use the first matching route it finds.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create a catch-all route in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can create a catch-all route by using a very general pattern at the end of your urls dictionary. Example:\\n```python\\nfrom app.apis import not_found_api\\nurls = {\\n    'user/': user_api,\\n    'book/': book_api,\\n    '': not_found_api,  # Catch-all route\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I debug routing issues in Panther?\"}, {\"role\": \"assistant\", \"content\": \"To debug routing issues, check the order of your routes (more specific first), verify URL patterns match exactly, ensure API functions are properly imported, and check that URL parameters are correctly defined. Panther will log routing information in debug mode.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create RESTful routes in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Create RESTful routes by organizing URLs hierarchically and using appropriate HTTP methods. Example:\\n```python\\nfrom app.apis import UserListAPI, UserDetailAPI\\nurls = {\\n    'users/': UserListAPI,      # GET /users/, POST /users/\\n    'users/<id>/': UserDetailAPI,  # GET /users/1/, PUT /users/1/, DELETE /users/1/\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle route middleware in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Middleware in Panther is applied globally or per-API, not per-route. If you need different middleware for different route groups, you can apply middleware to the specific API functions or use global middleware with conditional logic.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create route aliases in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can create route aliases by defining multiple URL patterns that point to the same API function. Example:\\n```python\\nfrom app.apis import user_api\\nurls = {\\n    'user/': user_api,\\n    'users/': user_api,  # Alias\\n    'u/': user_api,      # Short alias\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle route caching in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Route caching is handled automatically by Panther. The routing table is compiled once at startup and cached for performance. You don't need to manage route caching manually.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the best practice for organizing Panther routes?\"}, {\"role\": \"assistant\", \"content\": \"Organize routes by feature or domain. Use consistent naming conventions. Group related endpoints together. Use versioning for API evolution. Keep route patterns simple and RESTful. Document your route structure for team collaboration.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What are path variables in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Path variables in Panther are dynamic parts of URLs that capture values from the URL path and pass them to your API functions as parameters. They are defined using angle brackets (<variable_name>) in route patterns and allow you to create dynamic, parameterized endpoints.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I define path variables in Panther routes?\"}, {\"role\": \"assistant\", \"content\": \"Use angle brackets with a variable name in your route pattern. The variable name will be passed as a parameter to your API function. Example:\\n```python\\nfrom app.apis import user_detail_api\\nurls = {\\n    'user/<id>/': user_detail_api,\\n}\\n# The API function receives the id parameter:\\nasync def user_detail_api(request, id):\\n    return {'user_id': id}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use multiple path variables in a single route?\"}, {\"role\": \"assistant\", \"content\": \"Define multiple path variables in the URL pattern. They will be passed to your API function in the order they appear in the URL. Example:\\n```python\\nfrom app.apis import book_chapter_api\\nurls = {\\n    'book/<book_id>/chapter/<chapter_id>/': book_chapter_api,\\n}\\n# The API function receives both parameters:\\nasync def book_chapter_api(request, book_id, chapter_id):\\n    return {'book_id': book_id, 'chapter_id': chapter_id}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What types of values can path variables capture in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Path variables in Panther capture string values from the URL. You can convert them to other types (int, float, etc.) in your API function logic. Panther doesn't enforce type constraints on path variables at the routing level.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I convert path variables to specific data types in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Convert path variables to specific types in your API function. Example:\\n```python\\nasync def user_detail_api(request, id):\\n    try:\\n        user_id = int(id)\\n        user = await User.find_one(id=user_id)\\n        return user\\n    except ValueError:\\n        raise NotFoundAPIError('Invalid user ID')\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I validate path variables in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Validate path variables in your API function logic. Check for valid formats, ranges, or existence in the database. Example:\\n```python\\nasync def user_detail_api(request, id):\\n    if not id.isdigit():\\n        raise NotFoundAPIError('Invalid user ID format')\\n    user_id = int(id)\\n    if user_id <= 0:\\n        raise NotFoundAPIError('User ID must be positive')\\n    user = await User.find_one(id=user_id)\\n    if not user:\\n        raise NotFoundAPIError('User not found')\\n    return user\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I use path variables with nested routes in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Yes, you can use path variables in nested routes. The variables will be passed to the appropriate API function based on the route structure. Example:\\n```python\\nurls = {\\n    'user/': {\\n        '<id>/': user_detail_api,\\n        '<id>/posts/': user_posts_api,\\n        '<id>/posts/<post_id>/': user_post_detail_api,\\n    }\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle optional path variables in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Panther doesn't support optional path variables in route definitions. Use query parameters for optional data or create multiple routes. Example:\\n```python\\n# Use query parameters for optional data\\nurls = {\\n    'user/<id>/': user_detail_api,\\n}\\n# In your API function:\\nasync def user_detail_api(request, id):\\n    include_posts = request.query_params.get('include_posts', 'false').lower() == 'true'\\n    # Handle optional include_posts parameter\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create RESTful URLs with path variables in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use path variables to create RESTful resource URLs. Example:\\n```python\\nfrom app.apis import UserListAPI, UserDetailAPI, UserPostsAPI\\nurls = {\\n    'users/': UserListAPI,           # GET /users/, POST /users/\\n    'users/<id>/': UserDetailAPI,    # GET /users/1/, PUT /users/1/, DELETE /users/1/\\n    'users/<id>/posts/': UserPostsAPI,  # GET /users/1/posts/\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle path variable conflicts in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Routes are matched in order, so define more specific routes before general ones. Panther will use the first matching route. Example:\\n```python\\nurls = {\\n    'user/admin/': admin_api,        # More specific first\\n    'user/<id>/': user_detail_api,   # More general second\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I access path variables in class-based APIs in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Path variables are passed as parameters to your API methods in class-based APIs. Example:\\n```python\\nfrom panther.app import GenericAPI\\nclass UserAPI(GenericAPI):\\n    async def get(self, request, id):\\n        user_id = int(id)\\n        user = await User.find_one(id=user_id)\\n        return user\\n    async def put(self, request, id):\\n        user_id = int(id)\\n        # Update user logic\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use path variables with Generic APIs in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Generic APIs like RetrieveAPI, UpdateAPI, and DeleteAPI automatically handle path variables. The variable is typically used as the primary key. Example:\\n```python\\nfrom panther.generics import RetrieveAPI\\nclass UserDetailAPI(RetrieveAPI):\\n    model = User\\n    # The path variable will be used as the lookup key\\nurls = {\\n    'user/<id>/': UserDetailAPI,\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle path variables with authentication in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Path variables are available after authentication. You can use them along with the authenticated user information. Example:\\n```python\\nasync def user_detail_api(request, id):\\n    user_id = int(id)\\n    # Check if user can access this resource\\n    if request.user.id != user_id and not request.user.is_admin:\\n        raise AuthorizationAPIError('Access denied')\\n    user = await User.find_one(id=user_id)\\n    return user\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I debug path variable issues in Panther?\"}, {\"role\": \"assistant\", \"content\": \"To debug path variable issues, check that variable names match between route definition and API function parameters, verify the order of parameters matches the URL pattern, and ensure proper type conversion and validation in your API function.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create SEO-friendly URLs with path variables in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use descriptive path variables and combine them with slugs or titles. Example:\\n```python\\nurls = {\\n    'blog/<year>/<month>/<slug>/': blog_post_api,\\n    'product/<category>/<product_id>/': product_detail_api,\\n}\\n# URLs like: /blog/2024/01/my-awesome-post/\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle path variables with special characters in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Path variables capture the raw string from the URL. Handle special characters, encoding, or URL-encoded values in your API function logic. Example:\\n```python\\nimport urllib.parse\\nasync def search_api(request, query):\\n    decoded_query = urllib.parse.unquote(query)\\n    # Process the decoded query\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create hierarchical path variables in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Create hierarchical structures by nesting path variables. Example:\\n```python\\nurls = {\\n    'organization/<org_id>/': {\\n        'department/<dept_id>/': {\\n            'employee/<emp_id>/': employee_detail_api,\\n        }\\n    }\\n}\\n# URL: /organization/123/department/456/employee/789/\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle path variables with default values in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Since Panther doesn't support optional path variables, use query parameters or create multiple routes for different scenarios. Example:\\n```python\\nurls = {\\n    'user/<id>/': user_detail_api,\\n    'user/': user_list_api,  # Default behavior\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the best practice for naming path variables in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use descriptive, lowercase names with underscores for multi-word variables. Be consistent across your application. Use singular nouns for resource identifiers. Examples: user_id, book_title, post_slug, category_name.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I access the request object in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"To access the request object in Panther APIs, you must annotate the request parameter with the Request type. This tells Panther to inject the request object. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\n@API()\\nasync def my_api(request: Request):\\n    # Now you can use the request object\\n    return {'method': request.method}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What happens if I don't annotate the request parameter with Request?\"}, {\"role\": \"assistant\", \"content\": \"If you don't annotate the request parameter with Request, Panther will not inject the request object. The parameter will be treated as a regular parameter and will be None or cause an error. Always use `request: Request` annotation when you need access to the request object.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What information can I access from the request object in Panther?\"}, {\"role\": \"assistant\", \"content\": \"The request object provides access to:\\n- request.method: HTTP method (GET, POST, etc.)\\n- request.data: Request body data\\n- request.query_params: Query parameters\\n- request.headers: Request headers\\n- request.user: Authenticated user (if authentication is enabled)\\n- request.validated_data: Validated data from input_model\\n- request.path: Request path\\n- request.url: Full request URL\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I access query parameters in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Access query parameters through request.query_params. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\n@API()\\nasync def search_api(request: Request):\\n    query = request.query_params.get('q', '')\\n    page = int(request.query_params.get('page', 1))\\n    limit = int(request.query_params.get('limit', 10))\\n    return {'query': query, 'page': page, 'limit': limit}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I access request headers in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Access request headers through request.headers. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\n@API()\\nasync def api_with_headers(request: Request):\\n    content_type = request.headers.get('content-type')\\n    user_agent = request.headers.get('user-agent')\\n    authorization = request.headers.get('authorization')\\n    return {'content_type': content_type, 'user_agent': user_agent}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I access the request body data in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Access request body data through request.data. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\n@API()\\nasync def create_user(request: Request):\\n    if request.method == 'POST':\\n        user_data = request.data\\n        # Process the raw request data\\n        return {'received_data': user_data}\\n    return {'message': 'Use POST method'}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I access validated data from input_model in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Access validated data through request.validated_data when using input_model. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\nfrom app.serializers import UserSerializer\\n@API(input_model=UserSerializer)\\nasync def create_user(request: Request):\\n    validated_data = request.validated_data\\n    # validated_data is already validated and parsed\\n    user = await User.insert_one(**validated_data.model_dump())\\n    return user\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I access the authenticated user in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Access the authenticated user through request.user. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\nfrom panther.authentications import JWTAuthentication\\n@API(auth=JWTAuthentication)\\nasync def user_profile(request: Request):\\n    user = request.user\\n    if user:\\n        return {'username': user.username, 'email': user.email}\\n    return {'message': 'Not authenticated'}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle different HTTP methods in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Use request.method to check the HTTP method and handle each case. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\n@API(methods=['GET', 'POST', 'PUT', 'DELETE'])\\nasync def user_api(request: Request):\\n    match request.method:\\n        case 'GET':\\n            return {'action': 'get users'}\\n        case 'POST':\\n            return {'action': 'create user'}\\n        case 'PUT':\\n            return {'action': 'update user'}\\n        case 'DELETE':\\n            return {'action': 'delete user'}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I access request path and URL information in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Access path and URL information through request.path and request.url. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\n@API()\\nasync def path_info_api(request: Request):\\n    path = request.path\\n    full_url = request.url\\n    return {\\n        'path': path,\\n        'full_url': full_url,\\n        'method': request.method\\n    }\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle request data validation in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Use input_model for automatic validation. Panther will validate request.data and provide validated_data. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\nfrom app.serializers import UserSerializer\\n@API(input_model=UserSerializer)\\nasync def create_user(request: Request):\\n    try:\\n        validated_data = request.validated_data\\n        # Data is already validated\\n        user = await User.insert_one(**validated_data.model_dump())\\n        return user\\n    except Exception as e:\\n        return {'error': str(e)}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I access request data without input_model in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Access raw request data through request.data and handle validation manually. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\n@API()\\nasync def manual_validation_api(request: Request):\\n    if request.method == 'POST':\\n        raw_data = request.data\\n        # Manual validation logic here\\n        if not raw_data.get('username'):\\n            return {'error': 'Username is required'}\\n        return {'data': raw_data}\\n    return {'message': 'Use POST method'}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle file uploads in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Access file uploads through request.data. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\n@API()\\nasync def upload_file(request: Request):\\n    if request.method == 'POST':\\n        file_data = request.data.get('file')\\n        if file_data:\\n            # Handle file upload logic\\n            return {'message': 'File uploaded successfully'}\\n        return {'error': 'No file provided'}\\n    return {'message': 'Use POST method'}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I access request cookies in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Access cookies through request.headers. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\n@API()\\nasync def cookie_api(request: Request):\\n    cookie_header = request.headers.get('cookie', '')\\n    # Parse cookie header manually or use a cookie parser\\n    return {'cookies': cookie_header}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle request errors in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Use try-catch blocks to handle request processing errors. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\nfrom panther.exceptions import APIError\\n@API()\\nasync def error_handling_api(request: Request):\\n    try:\\n        data = request.data\\n        # Process data\\n        return {'success': True}\\n    except ValueError as e:\\n        raise APIError('Invalid data format', status_code=400)\\n    except Exception as e:\\n        raise APIError('Internal server error', status_code=500)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I access request metadata in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Access various request metadata through the request object. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\n@API()\\nasync def metadata_api(request: Request):\\n    return {\\n        'method': request.method,\\n        'path': request.path,\\n        'url': request.url,\\n        'headers': dict(request.headers),\\n        'query_params': dict(request.query_params),\\n        'user': request.user.username if request.user else None\\n    }\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use request data with path variables in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Combine path variables with request data. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\n@API()\\nasync def user_update_api(request: Request, user_id):\\n    if request.method == 'PUT':\\n        update_data = request.data\\n        # Update user with user_id using update_data\\n        return {'user_id': user_id, 'updated_data': update_data}\\n    return {'message': 'Use PUT method'}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the best practice for using the request object in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Always annotate the request parameter with Request type. Use input_model for automatic validation when possible. Handle different HTTP methods appropriately. Access user information safely (check if request.user exists). Use try-catch blocks for error handling. Keep request processing logic clean and focused.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create responses in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"In Panther APIs, you can return data directly or use the Response class for more control. Panther automatically wraps your return value in a Response object. Example:\\n```python\\nfrom panther.app import API\\n@API()\\nasync def simple_api():\\n    return {'message': 'Hello World'}  # Automatically wrapped in Response\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use the Response class in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Import and use the Response class for more control over your API responses. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import Response\\nfrom panther import status\\n@API()\\nasync def custom_response_api():\\n    return Response(\\n        data={'message': 'Success'},\\n        status_code=status.HTTP_201_CREATED,\\n        headers={'X-Custom-Header': 'value'}\\n    )\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What are the parameters of the Response class in Panther?\"}, {\"role\": \"assistant\", \"content\": \"The Response class accepts these parameters:\\n- data: The response data (dict, list, or any serializable object)\\n- status_code: HTTP status code (default: 200)\\n- headers: Custom response headers (dict)\\n- cookies: Response cookies (dict)\\nExample:\\n```python\\nfrom panther.response import Response\\nResponse(\\n    data={'result': 'success'},\\n    status_code=201,\\n    headers={'Content-Type': 'application/json'},\\n    cookies={'session_id': 'abc123'}\\n)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I set custom status codes in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Use the Response class with a custom status_code or return a tuple with (data, status_code). Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import Response\\nfrom panther import status\\n@API()\\nasync def status_code_api():\\n    # Method 1: Using Response class\\n    return Response(data={'created': True}, status_code=status.HTTP_201_CREATED)\\n    # Method 2: Using tuple\\n    return {'created': True}, 201\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I add custom headers to Panther API responses?\"}, {\"role\": \"assistant\", \"content\": \"Use the Response class with the headers parameter. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import Response\\n@API()\\nasync def headers_api():\\n    return Response(\\n        data={'message': 'Success'},\\n        headers={\\n            'X-Custom-Header': 'custom-value',\\n            'Cache-Control': 'no-cache',\\n            'Content-Type': 'application/json'\\n        }\\n    )\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I set cookies in Panther API responses?\"}, {\"role\": \"assistant\", \"content\": \"Use the Response class with the cookies parameter. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import Response\\n@API()\\nasync def cookie_api():\\n    return Response(\\n        data={'message': 'Cookie set'},\\n        cookies={\\n            'session_id': 'abc123',\\n            'user_preference': 'dark_mode'\\n        }\\n    )\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle different response types in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Return different data types based on your needs. Panther handles serialization automatically. Example:\\n```python\\nfrom panther.app import API\\n@API()\\nasync def response_types_api():\\n    # Return dict\\n    return {'message': 'dict response'}\\n    # Return list\\n    return [1, 2, 3, 4, 5]\\n    # Return string\\n    return 'string response'\\n    # Return None (204 No Content)\\n    return None\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create error responses in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Raise Panther exceptions or use Response with error status codes. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import Response\\nfrom panther.exceptions import NotFoundAPIError, APIError\\n@API()\\nasync def error_responses_api():\\n    # Method 1: Raise exceptions\\n    if not found:\\n        raise NotFoundAPIError('Resource not found')\\n    # Method 2: Use Response with error status\\n    return Response(\\n        data={'error': 'Custom error message'},\\n        status_code=400\\n    )\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I return paginated responses in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Structure your response data to include pagination information. Example:\\n```python\\nfrom panther.app import API\\n@API()\\nasync def paginated_api():\\n    page = 1\\n    limit = 10\\n    total = 100\\n    data = [{'id': i} for i in range(1, 11)]\\n    return {\\n        'data': data,\\n        'pagination': {\\n            'page': page,\\n            'limit': limit,\\n            'total': total,\\n            'pages': (total + limit - 1) // limit\\n        }\\n    }\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle file downloads in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Use Response with appropriate headers for file downloads. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import Response\\n@API()\\nasync def download_file_api():\\n    file_content = b'file content here'\\n    return Response(\\n        data=file_content,\\n        headers={\\n            'Content-Type': 'application/octet-stream',\\n            'Content-Disposition': 'attachment; filename=\\\"file.txt\\\"'\\n        }\\n    )\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I return JSON responses in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Panther automatically serializes your response data to JSON. Example:\\n```python\\nfrom panther.app import API\\n@API()\\nasync def json_api():\\n    return {\\n        'status': 'success',\\n        'data': {\\n            'id': 1,\\n            'name': 'John Doe',\\n            'email': 'john@example.com'\\n        },\\n        'timestamp': '2024-01-01T00:00:00Z'\\n    }\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle conditional responses in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Return different responses based on conditions. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import Response\\n@API()\\nasync def conditional_api(request: Request):\\n    if request.method == 'GET':\\n        return {'message': 'GET response'}\\n    elif request.method == 'POST':\\n        return Response(\\n            data={'message': 'POST response'},\\n            status_code=201\\n        )\\n    else:\\n        return Response(\\n            data={'error': 'Method not allowed'},\\n            status_code=405\\n        )\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I return empty responses in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Return None or empty data structures for empty responses. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import Response\\n@API()\\nasync def empty_responses_api():\\n    # Method 1: Return None (204 No Content)\\n    return None\\n    # Method 2: Return empty dict\\n    return {}\\n    # Method 3: Return empty list\\n    return []\\n    # Method 4: Use Response with empty data\\n    return Response(data={}, status_code=204)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle response serialization in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Panther automatically handles serialization. Use output_model for structured responses. Example:\\n```python\\nfrom panther.app import API\\nfrom app.serializers import UserSerializer\\n@API(output_model=UserSerializer)\\nasync def serialized_api():\\n    user_data = {\\n        'id': 1,\\n        'username': 'john_doe',\\n        'email': 'john@example.com'\\n    }\\n    return user_data  # Will be serialized using UserSerializer\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create streaming responses in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Return data that can be iterated over for streaming responses. Example:\\n```python\\nfrom panther.app import API\\n@API()\\nasync def streaming_api():\\n    def generate_data():\\n        for i in range(10):\\n            yield f'data chunk {i}\\\\n'\\n    return generate_data()\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle response caching headers in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Set cache control headers using the Response class. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import Response\\n@API()\\nasync def cached_api():\\n    return Response(\\n        data={'data': 'cached content'},\\n        headers={\\n            'Cache-Control': 'public, max-age=3600',\\n            'ETag': 'abc123',\\n            'Last-Modified': 'Wed, 01 Jan 2024 00:00:00 GMT'\\n        }\\n    )\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I return binary data in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Return bytes or use Response with appropriate content type. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import Response\\n@API()\\nasync def binary_api():\\n    binary_data = b'\\\\x89PNG\\\\r\\\\n\\\\x1a\\\\n'  # PNG file header\\n    return Response(\\n        data=binary_data,\\n        headers={'Content-Type': 'image/png'}\\n    )\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle response compression in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Panther handles compression automatically based on client headers. You can also set compression headers manually. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import Response\\n@API()\\nasync def compressed_api():\\n    return Response(\\n        data={'large_data': 'x' * 1000},\\n        headers={'Content-Encoding': 'gzip'}\\n    )\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the best practice for creating responses in Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Use simple return statements for basic responses. Use Response class for custom status codes, headers, or cookies. Use output_model for structured data serialization. Handle errors with appropriate exceptions. Keep response data consistent and well-structured. Use appropriate HTTP status codes for different scenarios.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use Jinja templates in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Panther supports Jinja2 templates for rendering HTML responses. You can use TemplateResponse to render templates with context data. First, ensure Jinja2 is installed:\\n```bash\\npip install jinja2\\n```\\nThen use TemplateResponse in your APIs:\\n```python\\nfrom panther.app import API\\nfrom panther.response import TemplateResponse\\n@API()\\nasync def template_api():\\n    return TemplateResponse(\\n        template_name='index.html',\\n        context={'title': 'Welcome', 'users': []}\\n    )\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create a TemplateResponse in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Import TemplateResponse and use it to render Jinja templates. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import TemplateResponse\\n@API()\\nasync def render_template_api():\\n    context = {\\n        'title': 'My Website',\\n        'message': 'Hello from Panther!',\\n        'items': ['item1', 'item2', 'item3']\\n    }\\n    return TemplateResponse(\\n        template_name='page.html',\\n        context=context\\n    )\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Where should I place my Jinja templates in a Panther project?\"}, {\"role\": \"assistant\", \"content\": \"Create a 'templates' directory in your project root or app directory. Panther will look for templates in the 'templates' folder by default. Example structure:\\n```\\nmy_project/\\n├── app/\\n│   ├── apis.py\\n│   └── templates/\\n│       ├── base.html\\n│       ├── index.html\\n│       └── user/\\n│           ├── list.html\\n│           └── detail.html\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create a base template in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Create a base.html template that other templates can extend. Example:\\n```html\\n<!-- templates/base.html -->\\n<!DOCTYPE html>\\n<html>\\n<head>\\n    <title>{% block title %}{% endblock %}</title>\\n    <link rel=\\\"stylesheet\\\" href=\\\"/static/style.css\\\">\\n</head>\\n<body>\\n    <header>\\n        <nav>\\n            <a href=\\\"/\\\">Home</a>\\n            <a href=\\\"/users\\\">Users</a>\\n        </nav>\\n    </header>\\n    <main>\\n        {% block content %}{% endblock %}\\n    </main>\\n    <footer>\\n        <p>&copy; 2024 My App</p>\\n    </footer>\\n</body>\\n</html>\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I extend a base template in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use the extends directive to inherit from a base template. Example:\\n```html\\n<!-- templates/index.html -->\\n{% extends \\\"base.html\\\" %}\\n\\n{% block title %}Home Page{% endblock %}\\n\\n{% block content %}\\n    <h1>Welcome to {{ title }}</h1>\\n    <p>{{ message }}</p>\\n    <ul>\\n    {% for item in items %}\\n        <li>{{ item }}</li>\\n    {% endfor %}\\n    </ul>\\n{% endblock %}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I pass data to Jinja templates in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Pass data through the context parameter in TemplateResponse. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import TemplateResponse\\nfrom app.models import User\\n@API()\\nasync def user_list_api():\\n    users = await User.find()\\n    return TemplateResponse(\\n        template_name='user/list.html',\\n        context={\\n            'users': users,\\n            'title': 'User List',\\n            'count': len(users)\\n        }\\n    )\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use Jinja template inheritance in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Create a hierarchy of templates using extends and blocks. Example:\\n```html\\n<!-- templates/admin/base.html -->\\n<!DOCTYPE html>\\n<html>\\n<head>\\n    <title>{% block title %}Admin{% endblock %}</title>\\n</head>\\n<body>\\n    <nav>Admin Navigation</nav>\\n    {% block content %}{% endblock %}\\n</body>\\n</html>\\n\\n<!-- templates/admin/dashboard.html -->\\n{% extends \\\"admin/base.html\\\" %}\\n{% block title %}Admin Dashboard{% endblock %}\\n{% block content %}\\n    <h1>Dashboard</h1>\\n    <p>Welcome, {{ user.username }}</p>\\n{% endblock %}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I include other templates in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use the include directive to include other template files. Example:\\n```html\\n<!-- templates/user/detail.html -->\\n{% extends \\\"base.html\\\" %}\\n\\n{% block content %}\\n    <h1>{{ user.username }}</h1>\\n    {% include \\\"user/profile.html\\\" %}\\n    {% include \\\"user/posts.html\\\" with context %}\\n{% endblock %}\\n\\n<!-- templates/user/profile.html -->\\n<div class=\\\"profile\\\">\\n    <p>Email: {{ user.email }}</p>\\n    <p>Age: {{ user.age }}</p>\\n</div>\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use Jinja macros in Panther templates?\"}, {\"role\": \"assistant\", \"content\": \"Define and use macros for reusable template components. Example:\\n```html\\n<!-- templates/macros.html -->\\n{% macro render_user_card(user) %}\\n    <div class=\\\"user-card\\\">\\n        <h3>{{ user.username }}</h3>\\n        <p>{{ user.email }}</p>\\n        <a href=\\\"/user/{{ user.id }}\\\">View Profile</a>\\n    </div>\\n{% endmacro %}\\n\\n<!-- templates/user/list.html -->\\n{% extends \\\"base.html\\\" %}\\n{% from \\\"macros.html\\\" import render_user_card %}\\n\\n{% block content %}\\n    <h1>Users</h1>\\n    {% for user in users %}\\n        {{ render_user_card(user) }}\\n    {% endfor %}\\n{% endblock %}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle forms in Jinja templates with Panther?\"}, {\"role\": \"assistant\", \"content\": \"Create forms in templates and handle them in your APIs. Example:\\n```html\\n<!-- templates/user/create.html -->\\n{% extends \\\"base.html\\\" %}\\n\\n{% block content %}\\n    <form method=\\\"POST\\\" action=\\\"/user/create\\\">\\n        <div>\\n            <label for=\\\"username\\\">Username:</label>\\n            <input type=\\\"text\\\" id=\\\"username\\\" name=\\\"username\\\" required>\\n        </div>\\n        <div>\\n            <label for=\\\"email\\\">Email:</label>\\n            <input type=\\\"email\\\" id=\\\"email\\\" name=\\\"email\\\" required>\\n        </div>\\n        <button type=\\\"submit\\\">Create User</button>\\n    </form>\\n{% endblock %}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use Jinja filters in Panther templates?\"}, {\"role\": \"assistant\", \"content\": \"Use built-in Jinja filters or create custom ones. Example:\\n```html\\n<!-- templates/user/list.html -->\\n{% extends \\\"base.html\\\" %}\\n\\n{% block content %}\\n    <h1>Users ({{ users|length }})</h1>\\n    {% for user in users|sort(attribute='username') %}\\n        <div>\\n            <h3>{{ user.username|title }}</h3>\\n            <p>Created: {{ user.created_at|datetime }}</p>\\n            <p>Status: {{ user.is_active|yesno('Active', 'Inactive') }}</p>\\n        </div>\\n    {% endfor %}\\n{% endblock %}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle template errors in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use try-catch blocks to handle template rendering errors. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import TemplateResponse\\nfrom panther.exceptions import APIError\\n@API()\\nasync def safe_template_api():\\n    try:\\n        return TemplateResponse(\\n            template_name='page.html',\\n            context={'data': 'some data'}\\n        )\\n    except Exception as e:\\n        # Fallback to JSON response\\n        return {'error': 'Template rendering failed', 'message': str(e)}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use static files with Jinja templates in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Reference static files in your templates and serve them from a static directory. Example:\\n```html\\n<!-- templates/base.html -->\\n<!DOCTYPE html>\\n<html>\\n<head>\\n    <title>{% block title %}{% endblock %}</title>\\n    <link rel=\\\"stylesheet\\\" href=\\\"/static/css/style.css\\\">\\n    <script src=\\\"/static/js/app.js\\\" defer></script>\\n</head>\\n<body>\\n    <img src=\\\"/static/images/logo.png\\\" alt=\\\"Logo\\\">\\n    {% block content %}{% endblock %}\\n</body>\\n</html>\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create custom Jinja filters in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can extend Jinja's environment with custom filters. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import TemplateResponse\\nfrom datetime import datetime\\n\\ndef format_date(value, format='%Y-%m-%d'):\\n    if isinstance(value, datetime):\\n        return value.strftime(format)\\n    return value\\n\\n# Register custom filter with Jinja environment\\n# This would typically be done in your app configuration\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use conditional rendering in Jinja templates with Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use if/elif/else statements for conditional rendering. Example:\\n```html\\n<!-- templates/user/detail.html -->\\n{% extends \\\"base.html\\\" %}\\n\\n{% block content %}\\n    <h1>{{ user.username }}</h1>\\n    \\n    {% if user.is_admin %}\\n        <div class=\\\"admin-badge\\\">Administrator</div>\\n    {% endif %}\\n    \\n    {% if user.posts %}\\n        <h2>Posts ({{ user.posts|length }})</h2>\\n        {% for post in user.posts %}\\n            <div class=\\\"post\\\">{{ post.title }}</div>\\n        {% endfor %}\\n    {% else %}\\n        <p>No posts yet.</p>\\n    {% endif %}\\n    \\n    {% if user.is_active and user.email_verified %}\\n        <p>Account is fully verified.</p>\\n    {% elif user.is_active %}\\n        <p>Please verify your email.</p>\\n    {% else %}\\n        <p>Account is inactive.</p>\\n    {% endif %}\\n{% endblock %}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle template caching in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Panther automatically caches compiled templates for performance. In development, templates are reloaded when modified. For production, templates are cached to improve response times.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create a template with dynamic content in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Pass dynamic data through context and use Jinja expressions. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.response import TemplateResponse\\nfrom app.models import Post\\n@API()\\nasync def dynamic_content_api(request: Request, category=None):\\n    posts = await Post.find(category=category) if category else await Post.find()\\n    return TemplateResponse(\\n        template_name='blog/list.html',\\n        context={\\n            'posts': posts,\\n            'category': category,\\n            'total_posts': len(posts),\\n            'current_user': request.user\\n        }\\n    )\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the best practice for organizing Jinja templates in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Organize templates by feature or module. Use a clear hierarchy with base templates. Keep templates focused and reusable. Use includes and macros for common components. Follow consistent naming conventions. Separate concerns between templates and business logic.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does OpenAPI documentation work in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Panther automatically generates OpenAPI documentation for your APIs. It analyzes your API definitions, input/output models, and source code to create comprehensive API documentation. Panther supports multiple OpenAPI UI types and provides built-in documentation endpoints.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I enable OpenAPI documentation in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Add OpenAPI URLs to your main URL configuration. Example:\\n```python\\nfrom panther.openapi.urls import url_routing as openapi_url_routing\\nurl_routing = {\\n    'docs/': openapi_url_routing,\\n}\\n```\\nThis will provide access to all OpenAPI UI types at /docs/.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What OpenAPI UI types does Panther support?\"}, {\"role\": \"assistant\", \"content\": \"Panther supports several OpenAPI UI types:\\n- ScalarOpenAPI: Modern, fast OpenAPI UI\\n- SwaggerOpenAPI: Classic Swagger UI\\n- RedocOpenAPI: Clean, responsive documentation\\n- RapiDocOpenAPI: Lightweight, fast UI\\n- SpotlightOpenAPI: Modern, feature-rich UI\\n\\nYou can use them individually or all together via openapi_url_routing.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use output_schema for OpenAPI documentation in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use output_schema to explicitly define response models and status codes for OpenAPI docs. Example:\\n```python\\nfrom panther import status\\nfrom panther.app import API\\nfrom panther.openapi import OutputSchema\\nfrom app.serializers import UserSerializer\\n@API(output_schema=OutputSchema(model=UserSerializer, status_code=status.HTTP_201_CREATED))\\nasync def create_user():\\n    return user\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use output_model for OpenAPI documentation in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use output_model to specify the response serializer. Panther will use this for OpenAPI docs and response serialization. Example:\\n```python\\nfrom panther.app import API\\nfrom app.serializers import UserSerializer\\n@API(output_model=UserSerializer)\\nasync def get_user():\\n    return user\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How does Panther determine response models and status codes for OpenAPI docs?\"}, {\"role\": \"assistant\", \"content\": \"Panther follows this priority order:\\n1. output_schema: Most accurate, specifies both model and status code\\n2. output_model: Specifies serializer, Panther extracts status code from source code\\n3. Source Code Analysis: Panther analyzes your code to determine response structure\\n\\nFor best results, always use output_schema.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I document multiple response types in Panther OpenAPI?\"}, {\"role\": \"assistant\", \"content\": \"Currently, output_schema documents a single response per API. For multiple responses (e.g., success/error), you may need to extend the OpenAPI docs manually or use different APIs for different scenarios.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I add descriptions to my APIs in OpenAPI docs?\"}, {\"role\": \"assistant\", \"content\": \"Use docstrings in your API functions to add descriptions. Panther will extract these for OpenAPI documentation. Example:\\n```python\\n@API()\\nasync def get_user(request: Request, user_id: int):\\n    \\\"\\\"\\\"\\n    Retrieve a user by ID.\\n    \\n    Returns user information including profile details.\\n    \\\"\\\"\\\"\\n    return user\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I document path parameters in Panther OpenAPI?\"}, {\"role\": \"assistant\", \"content\": \"Path parameters are automatically detected from your route patterns and function parameters. Panther will document them in the OpenAPI spec. Example:\\n```python\\n@API()\\nasync def user_detail_api(request: Request, user_id: int):\\n    \\\"\\\"\\\"Get user details by ID\\\"\\\"\\\"\\n    return user\\n# Route: 'user/<user_id>/'\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I document query parameters in Panther OpenAPI?\"}, {\"role\": \"assistant\", \"content\": \"Query parameters are documented based on your input_model or function parameters. Use Pydantic models with Field descriptions for better documentation. Example:\\n```python\\nfrom pydantic import BaseModel, Field\\nclass UserQueryParams(BaseModel):\\n    page: int = Field(1, description=\\\"Page number\\\")\\n    limit: int = Field(10, description=\\\"Items per page\\\")\\n    search: str = Field(None, description=\\\"Search term\\\")\\n\\n@API(input_model=UserQueryParams)\\nasync def list_users(request: Request):\\n    return users\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I document request body in Panther OpenAPI?\"}, {\"role\": \"assistant\", \"content\": \"Use input_model to document request body. Panther will use this for OpenAPI documentation. Example:\\n```python\\nfrom panther.app import API\\nfrom app.serializers import UserCreateSerializer\\n@API(input_model=UserCreateSerializer)\\nasync def create_user(request: Request):\\n    data = request.validated_data\\n    return user\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I add tags to my APIs in OpenAPI docs?\"}, {\"role\": \"assistant\", \"content\": \"Use the tags parameter in your API decorator to group related endpoints. Example:\\n```python\\n@API(tags=['users'])\\nasync def get_user():\\n    return user\\n\\n@API(tags=['users', 'authentication'])\\nasync def create_user():\\n    return user\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I add operation IDs to my APIs in OpenAPI docs?\"}, {\"role\": \"assistant\", \"content\": \"Use the operation_id parameter in your API decorator. Example:\\n```python\\n@API(operation_id='getUserById')\\nasync def get_user(request: Request, user_id: int):\\n    return user\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I customize OpenAPI info in Panther?\"}, {\"role\": \"assistant\", \"content\": \"You can customize OpenAPI metadata by configuring the OpenAPI environment. Example:\\n```python\\n# In your configs\\nOPENAPI_INFO = {\\n    'title': 'My API',\\n    'version': '1.0.0',\\n    'description': 'My awesome API',\\n    'contact': {\\n        'name': 'API Support',\\n        'email': 'support@example.com'\\n    }\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I add security schemes to OpenAPI docs in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Security schemes are automatically documented based on your authentication configuration. Panther will include JWT authentication in the OpenAPI spec when you use JWTAuthentication.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I test APIs using OpenAPI docs in Panther?\"}, {\"role\": \"assistant\", \"content\": \"OpenAPI UI provides interactive testing capabilities. You can:\\n- Try out API endpoints directly from the documentation\\n- Set authentication tokens\\n- Modify request parameters\\n- View response examples\\n- Test different HTTP methods\\n\\nVisit your docs endpoint (e.g., /docs/) to access the interactive interface.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I export OpenAPI specification in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Access the raw OpenAPI specification at /docs/openapi.json. This provides the complete OpenAPI spec in JSON format that can be imported into other tools or used for code generation.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle OpenAPI documentation for WebSockets in Panther?\"}, {\"role\": \"assistant\", \"content\": \"WebSockets are not typically documented in OpenAPI specs as they use different protocols. OpenAPI is designed for HTTP APIs. For WebSocket documentation, consider using separate documentation or code comments.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I add examples to OpenAPI documentation in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use Pydantic Field examples in your serializers. Example:\\n```python\\nfrom pydantic import BaseModel, Field\\nclass UserSerializer(BaseModel):\\n    username: str = Field(example=\\\"john_doe\\\")\\n    email: str = Field(example=\\\"john@example.com\\\")\\n    age: int = Field(example=25)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I document error responses in Panther OpenAPI?\"}, {\"role\": \"assistant\", \"content\": \"Panther automatically documents common error responses (400, 401, 403, 404, 500). For custom error responses, you may need to extend the OpenAPI documentation manually or use output_schema with different status codes.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I customize OpenAPI UI themes in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Different OpenAPI UI types have different customization options. You can modify the UI appearance by customizing the OpenAPI environment or using different UI types that better match your design requirements.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the best practice for OpenAPI documentation in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Always use output_schema for precise documentation. Add meaningful descriptions using docstrings. Use tags to organize related endpoints. Provide examples in your serializers. Keep documentation up to date with your API changes. Use consistent naming conventions for operation IDs and tags.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do events work in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Panther supports two specific events: startup and shutdown. These events allow you to run code when your application starts up or shuts down. You can use these events for initialization tasks, cleanup operations, and resource management.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use startup events in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use the @startup decorator to register functions that run when your application starts. Import it from panther.events and apply it to functions that should execute during startup. Example:\\n```python\\nfrom panther.events import startup\\n@startup\\nasync def initialize_database():\\n    # Initialize database connection\\n    await setup_database()\\n    print('Database initialized')\\n\\n@startup\\nasync def load_configuration():\\n    # Load application configuration\\n    await load_config()\\n    print('Configuration loaded')\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use shutdown events in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use the @shutdown decorator to register functions that run when your application shuts down. Import it from panther.events and apply it to functions that should execute during shutdown. Example:\\n```python\\nfrom panther.events import shutdown\\n@shutdown\\nasync def cleanup_database():\\n    # Close database connections\\n    await close_database_connections()\\n    print('Database connections closed')\\n\\n@shutdown\\nasync def save_state():\\n    # Save application state\\n    await save_application_state()\\n    print('Application state saved')\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What should I do in startup events in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Startup events are perfect for:\\n- Initializing database connections\\n- Loading configuration files\\n- Setting up external service connections\\n- Creating application caches\\n- Running database migrations\\n- Validating environment variables\\n- Setting up logging systems\\n\\nExample:\\n```python\\nfrom panther.events import startup\\n@startup\\nasync def app_initialization():\\n    # Validate environment\\n    validate_env_vars()\\n    # Setup database\\n    await setup_database()\\n    # Load configuration\\n    load_app_config()\\n    # Initialize cache\\n    initialize_cache()\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What should I do in shutdown events in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Shutdown events are perfect for:\\n- Closing database connections\\n- Saving application state\\n- Cleaning up temporary files\\n- Stopping background tasks\\n- Closing external service connections\\n- Flushing logs\\n- Graceful cleanup of resources\\n\\nExample:\\n```python\\nfrom panther.events import shutdown\\n@shutdown\\nasync def app_cleanup():\\n    # Close database connections\\n    await close_database()\\n    # Save state\\n    await save_state()\\n    # Cleanup temp files\\n    cleanup_temp_files()\\n    # Flush logs\\n    flush_logs()\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle errors in startup/shutdown events in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use try-catch blocks in your startup and shutdown functions to handle errors gracefully. Example:\\n```python\\nfrom panther.events import startup, shutdown\\n@startup\\nasync def safe_startup():\\n    try:\\n        await initialize_database()\\n        print('Database initialized successfully')\\n    except Exception as e:\\n        print(f'Failed to initialize database: {e}')\\n        # Decide whether to continue or exit\\n        raise\\n\\n@shutdown\\nasync def safe_shutdown():\\n    try:\\n        await cleanup_resources()\\n        print('Cleanup completed successfully')\\n    except Exception as e:\\n        print(f'Error during cleanup: {e}')\\n        # Log error but don't prevent shutdown\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"Can I have multiple startup/shutdown events in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Yes, you can have multiple startup and shutdown events. They will be executed in the order they are defined. Example:\\n```python\\nfrom panther.events import startup, shutdown\\n@startup\\nasync def first_startup_task():\\n    print('First startup task')\\n\\n@startup\\nasync def second_startup_task():\\n    print('Second startup task')\\n\\n@shutdown\\nasync def first_shutdown_task():\\n    print('First shutdown task')\\n\\n@shutdown\\nasync def second_shutdown_task():\\n    print('Second shutdown task')\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use startup events for database initialization in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use startup events to initialize database connections and run migrations. Example:\\n```python\\nfrom panther.events import startup\\nfrom panther.db import connect\\n@startup\\nasync def initialize_database():\\n    # Connect to database\\n    await connect()\\n    # Run migrations\\n    await run_migrations()\\n    # Create indexes\\n    await create_indexes()\\n    print('Database initialized and ready')\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use shutdown events for database cleanup in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use shutdown events to properly close database connections. Example:\\n```python\\nfrom panther.events import shutdown\\nfrom panther.db import close\\n@shutdown\\nasync def cleanup_database():\\n    # Close database connections\\n    await close()\\n    print('Database connections closed')\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use startup events for configuration loading in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use startup events to load and validate configuration. Example:\\n```python\\nfrom panther.events import startup\\nimport os\\n@startup\\nasync def load_configuration():\\n    # Load environment variables\\n    load_env_vars()\\n    # Validate required config\\n    validate_config()\\n    # Setup logging\\n    setup_logging()\\n    print('Configuration loaded successfully')\\n\\ndef validate_config():\\n    required_vars = ['DATABASE_URL', 'SECRET_KEY']\\n    for var in required_vars:\\n        if not os.getenv(var):\\n            raise ValueError(f'Missing required environment variable: {var}')\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What is the best practice for using startup/shutdown events in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Keep startup/shutdown functions focused and lightweight. Handle errors gracefully. Use startup for initialization and shutdown for cleanup. Don't perform heavy operations that could slow down startup. Log important events. Ensure shutdown events complete quickly to allow graceful application termination.\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I perform CRUD operations in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Panther provides async CRUD methods for database operations. Example:\\n```python\\nfrom app.models import User\\n\\n# Create\\nuser = await User.insert_one(username='john', email='john@example.com')\\n\\n# Read\\nusers = await User.find()  # Get all users\\nuser = await User.find_one(username='john')  # Get single user\\n\\n# Update\\nawait User.update_one({'username': 'john'}, {'email': 'new@example.com'})\\n\\n# Delete\\nawait User.delete_one({'username': 'john'})\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use query filters in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use dictionaries to specify query filters. Example:\\n```python\\nfrom app.models import User\\n\\n# Simple filters\\nusers = await User.find({'age': 25})\\nusers = await User.find({'is_active': True, 'city': 'New York'})\\n\\n# Complex filters with operators\\nusers = await User.find({\\n    'age': {'$gte': 18, '$lte': 65},\\n    'email': {'$regex': '@gmail.com'},\\n    'created_at': {'$gt': '2024-01-01'}\\n})\\n\\n# OR conditions\\nusers = await User.find({\\n    '$or': [\\n        {'age': {'$lt': 18}},\\n        {'age': {'$gt': 65}}\\n    ]\\n})\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What query operators does Panther support?\"}, {\"role\": \"assistant\", \"content\": \"Panther supports MongoDB-style query operators:\\n- Comparison: $eq, $ne, $gt, $gte, $lt, $lte\\n- Logical: $and, $or, $not, $nor\\n- Array: $in, $nin, $all, $elemMatch\\n- String: $regex, $options\\n- Existence: $exists\\n- Type: $type\\n\\nExample:\\n```python\\nfrom app.models import Product\\n\\n# Comparison operators\\nproducts = await Product.find({'price': {'$gte': 10, '$lte': 100}})\\n\\n# Array operators\\nproducts = await Product.find({'tags': {'$in': ['electronics', 'gadgets']}})\\n\\n# String regex\\nusers = await User.find({'email': {'$regex': '^admin@', '$options': 'i'}})\\n\\n# Logical operators\\nproducts = await Product.find({\\n    '$and': [\\n        {'price': {'$gt': 50}},\\n        {'category': 'electronics'}\\n    ]\\n})\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I use aggregation in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use the aggregate method for complex data processing. Example:\\n```python\\nfrom app.models import Order\\n\\n# Group by category and calculate totals\\nresult = await Order.aggregate([\\n    {'$group': {\\n        '_id': '$category',\\n        'total_sales': {'$sum': '$amount'},\\n        'count': {'$sum': 1}\\n    }},\\n    {'$sort': {'total_sales': -1}}\\n])\\n\\n# Match and group\\nresult = await Order.aggregate([\\n    {'$match': {'status': 'completed'}},\\n    {'$group': {\\n        '_id': '$user_id',\\n        'total_orders': {'$sum': 1},\\n        'total_amount': {'$sum': '$amount'}\\n    }},\\n    {'$sort': {'total_amount': -1}},\\n    {'$limit': 10}\\n])\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I handle database transactions in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use the transaction context manager for atomic operations. Example:\\n```python\\nfrom panther.db import transaction\\nfrom app.models import User, Account\\n\\nasync def create_user_with_account(user_data, account_data):\\n    async with transaction() as txn:\\n        # Create user\\n        user = await User.insert_one(**user_data, _transaction=txn)\\n        \\n        # Create account with user reference\\n        account = await Account.insert_one(\\n            user_id=user.id,\\n            **account_data,\\n            _transaction=txn\\n        )\\n        \\n        # If any operation fails, both are rolled back\\n        return user, account\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I manage database connections in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Panther automatically manages database connections. Use connect() and close() for manual control. Example:\\n```python\\nfrom panther.db import connect, close\\nfrom panther.events import startup, shutdown\\n\\n@startup\\nasync def initialize_database():\\n    await connect()\\n    print('Database connected')\\n\\n@shutdown\\nasync def cleanup_database():\\n    await close()\\n    print('Database disconnected')\\n\\n# Or manually in your code\\nasync def custom_db_operation():\\n    await connect()\\n    try:\\n        # Your database operations\\n        pass\\n    finally:\\n        await close()\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create custom serializer fields in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Create custom fields by inheriting from Field or using custom methods. Example:\\n```python\\nfrom panther.serializer import ModelSerializer, Field\\nfrom datetime import datetime\\n\\nclass UserSerializer(ModelSerializer):\\n    class Config:\\n        model = User\\n        fields = ['id', 'username', 'email', 'age', 'created_at']\\n    \\n    # Custom field with computed value\\n    @Field\\n    def display_name(self, obj):\\n        return f'{obj.username} ({obj.email})'\\n    \\n    # Custom field with formatting\\n    @Field\\n    def formatted_date(self, obj):\\n        return obj.created_at.strftime('%Y-%m-%d %H:%M:%S')\\n    \\n    # Custom field with conditional logic\\n    @Field\\n    def status(self, obj):\\n        if obj.is_active:\\n            return 'active'\\n        return 'inactive'\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create nested serializers in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use nested serializers for related objects. Example:\\n```python\\nfrom panther.serializer import ModelSerializer\\n\\nclass AddressSerializer(ModelSerializer):\\n    class Config:\\n        model = Address\\n        fields = ['street', 'city', 'zipcode']\\n\\nclass PostSerializer(ModelSerializer):\\n    class Config:\\n        model = Post\\n        fields = ['id', 'title', 'content', 'created_at']\\n\\nclass UserSerializer(ModelSerializer):\\n    class Config:\\n        model = User\\n        fields = ['id', 'username', 'email', 'address', 'posts']\\n    \\n    # Nested serializer for address\\n    address = AddressSerializer()\\n    \\n    # Nested serializer for posts (many)\\n    posts = PostSerializer(many=True)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I add validation to serializers in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use Pydantic validators and custom validation methods. Example:\\n```python\\nfrom panther.serializer import ModelSerializer\\nfrom pydantic import validator, field_validator\\n\\nclass UserSerializer(ModelSerializer):\\n    class Config:\\n        model = User\\n        fields = ['username', 'email', 'age', 'password']\\n    \\n    # Field validation\\n    @validator('username')\\n    def validate_username(cls, v):\\n        if len(v) < 3:\\n            raise ValueError('Username must be at least 3 characters')\\n        if not v.isalnum():\\n            raise ValueError('Username must be alphanumeric')\\n        return v\\n    \\n    @validator('email')\\n    def validate_email(cls, v):\\n        if '@' not in v:\\n            raise ValueError('Invalid email format')\\n        return v.lower()\\n    \\n    @validator('age')\\n    def validate_age(cls, v):\\n        if v < 0 or v > 150:\\n            raise ValueError('Age must be between 0 and 150')\\n        return v\\n    \\n    # Custom validation method\\n    def validate(self, data):\\n        if data.get('username') == data.get('password'):\\n            raise ValueError('Username and password cannot be the same')\\n        return data\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create read-only and write-only fields in Panther serializers?\"}, {\"role\": \"assistant\", \"content\": \"Use read_only and write_only parameters in Field decorators. Example:\\n```python\\nfrom panther.serializer import ModelSerializer, Field\\n\\nclass UserSerializer(ModelSerializer):\\n    class Config:\\n        model = User\\n        fields = ['id', 'username', 'email', 'password', 'created_at']\\n    \\n    # Read-only field (only in responses)\\n    @Field(read_only=True)\\n    def id(self, obj):\\n        return obj.id\\n    \\n    # Write-only field (only in requests)\\n    @Field(write_only=True)\\n    def password(self, obj):\\n        return obj.password if hasattr(obj, 'password') else None\\n    \\n    # Read-only computed field\\n    @Field(read_only=True)\\n    def created_at(self, obj):\\n        return obj.created_at.strftime('%Y-%m-%d %H:%M:%S')\\n    \\n    # Conditional field\\n    @Field(read_only=True)\\n    def is_admin(self, obj):\\n        return getattr(obj, 'is_admin', False)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I test Panther APIs?\"}, {\"role\": \"assistant\", \"content\": \"Use pytest with async support for testing Panther APIs. Example:\\n```python\\nimport pytest\\nfrom panther.test import TestClient\\nfrom main import app\\nfrom app.models import User\\n\\n@pytest.fixture\\nasync def client():\\n    async with TestClient(app) as client:\\n        yield client\\n\\n@pytest.fixture\\nasync def test_user():\\n    user = await User.insert_one(\\n        username='testuser',\\n        email='test@example.com'\\n    )\\n    yield user\\n    await User.delete_one({'id': user.id})\\n\\nasync def test_create_user(client):\\n    response = await client.post('/user/', json={\\n        'username': 'newuser',\\n        'email': 'new@example.com'\\n    })\\n    assert response.status_code == 201\\n    data = response.json()\\n    assert data['username'] == 'newuser'\\n\\nasync def test_get_user(client, test_user):\\n    response = await client.get(f'/user/{test_user.id}/')\\n    assert response.status_code == 200\\n    data = response.json()\\n    assert data['username'] == 'testuser'\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I test WebSockets in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use TestClient for WebSocket testing. Example:\\n```python\\nimport pytest\\nfrom panther.test import TestClient\\nfrom main import app\\n\\n@pytest.fixture\\nasync def client():\\n    async with TestClient(app) as client:\\n        yield client\\n\\nasync def test_websocket_connection(client):\\n    with client.websocket_connect('/ws/echo/') as websocket:\\n        # Send message\\n        websocket.send_text('Hello')\\n        \\n        # Receive response\\n        response = websocket.receive_text()\\n        assert response == 'Hello'\\n\\nasync def test_websocket_with_auth(client):\\n    # First authenticate\\n    auth_response = await client.post('/auth/login/', json={\\n        'username': 'testuser',\\n        'password': 'password'\\n    })\\n    token = auth_response.json()['access_token']\\n    \\n    # Connect with authentication\\n    with client.websocket_connect(\\n        f'/ws/chat/?authorization={token}'\\n    ) as websocket:\\n        websocket.send_json({'type': 'message', 'text': 'Hello'})\\n        response = websocket.receive_json()\\n        assert response['type'] == 'message'\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create test fixtures in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use pytest fixtures for test data setup and cleanup. Example:\\n```python\\nimport pytest\\nfrom app.models import User, Post, Category\\n\\n@pytest.fixture\\nasync def test_user():\\n    user = await User.insert_one(\\n        username='testuser',\\n        email='test@example.com',\\n        is_active=True\\n    )\\n    yield user\\n    await User.delete_one({'id': user.id})\\n\\n@pytest.fixture\\nasync def test_category():\\n    category = await Category.insert_one(\\n        name='Technology',\\n        slug='technology'\\n    )\\n    yield category\\n    await Category.delete_one({'id': category.id})\\n\\n@pytest.fixture\\nasync def test_post(test_user, test_category):\\n    post = await Post.insert_one(\\n        title='Test Post',\\n        content='Test content',\\n        author_id=test_user.id,\\n        category_id=test_category.id\\n    )\\n    yield post\\n    await Post.delete_one({'id': post.id})\\n\\n@pytest.fixture\\nasync def auth_headers(test_user):\\n    # Create authentication token\\n    token = create_test_token(test_user)\\n    return {'Authorization': f'Bearer {token}'}\\n\\nasync def test_post_creation(client, test_user, test_category, auth_headers):\\n    response = await client.post('/post/', \\n        json={\\n            'title': 'New Post',\\n            'content': 'New content',\\n            'category_id': test_category.id\\n        },\\n        headers=auth_headers\\n    )\\n    assert response.status_code == 201\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I set up environment-specific configurations in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Create configuration variables as normal variables in your configs file. Example:\\n```python\\n# core/configs.py\\nimport os\\nfrom datetime import timedelta\\n\\n# Development settings\\nDEBUG = True\\nLOG_QUERIES = True\\nALLOW_ORIGINS = ['*']\\nCACHE = None  # Disable caching in development\\n\\n# Production settings (override based on environment)\\nif os.getenv('ENVIRONMENT') == 'production':\\n    DEBUG = False\\n    LOG_QUERIES = False\\n    ALLOW_ORIGINS = [\\n        'https://yourdomain.com',\\n        'https://app.yourdomain.com'\\n    ]\\n    CACHE = timedelta(minutes=30)\\n    THROTTLING = Throttle(rate=100, duration=timedelta(minutes=1))\\n\\n# Database settings\\nDATABASE_URL = os.getenv('DATABASE_URL', 'mongodb://localhost:27017')\\nDATABASE_NAME = os.getenv('DATABASE_NAME', 'panther_db')\\n\\n# Security settings\\nSECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key')\\nJWT_ALGORITHM = 'HS256'\\nJWT_LIFE_TIME = timedelta(days=1)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I access configuration variables in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Access configuration variables using panther.configs.config. Example:\\n```python\\nfrom panther.configs import config\\n\\n# Access configuration variables\\ndebug_mode = config.DEBUG\\nsecret_key = config.SECRET_KEY\\ndatabase_url = config.DATABASE_URL\\n\\n# Use in your APIs\\n@API()\\nasync def debug_info():\\n    if config.DEBUG:\\n        return {'debug': True, 'environment': 'development'}\\n    return {'debug': False}\\n\\n# Use in middleware\\nclass ConfigMiddleware(HTTPMiddleware):\\n    async def __call__(self, request):\\n        if config.LOG_QUERIES:\\n            # Log database queries\\n            pass\\n        \\n        response = await self.dispatch(request=request)\\n        return response\\n\\n# Use in models\\nclass User(Model):\\n    username: str = Field(index=True)\\n    \\n    async def save(self):\\n        if config.DEBUG:\\n            print(f'Saving user: {self.username}')\\n        return await super().save()\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I validate configuration in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Add validation logic directly in your configs.py file. Example:\\n```python\\n# core/configs.py\\nimport os\\nfrom datetime import timedelta\\n\\n# Configuration variables\\nSECRET_KEY = os.getenv('SECRET_KEY')\\nDATABASE_URL = os.getenv('DATABASE_URL')\\nALLOW_ORIGINS = os.getenv('ALLOW_ORIGINS', '*').split(',')\\nDEBUG = os.getenv('DEBUG', 'False').lower() == 'true'\\n\\n# Validation functions\\ndef validate_config():\\n    errors = []\\n    \\n    # Validate SECRET_KEY\\n    if not SECRET_KEY or len(SECRET_KEY) < 32:\\n        errors.append('SECRET_KEY must be at least 32 characters')\\n    \\n    # Validate DATABASE_URL\\n    if not DATABASE_URL:\\n        errors.append('DATABASE_URL is required')\\n    elif not DATABASE_URL.startswith(('mongodb://', 'mongodb+srv://')):\\n        errors.append('Invalid DATABASE_URL format')\\n    \\n    # Validate ALLOW_ORIGINS\\n    if '*' in ALLOW_ORIGINS and len(ALLOW_ORIGINS) > 1:\\n        errors.append('Cannot use * with specific origins')\\n    \\n    if errors:\\n        raise ValueError(f'Configuration errors: {\\\\n'.join(errors)}')\\n\\n# Run validation on import\\nvalidate_config()\\n\\n# Additional computed values\\nCACHE_DURATION = timedelta(minutes=30) if not DEBUG else None\\nTHROTTLING_RATE = 100 if not DEBUG else 1000\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I manage secrets in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Store secrets as environment variables and access them through configs. Example:\\n```python\\n# core/configs.py\\nimport os\\nfrom cryptography.fernet import Fernet\\n\\n# Basic secret management\\nSECRET_KEY = os.getenv('SECRET_KEY')\\nDATABASE_URL = os.getenv('DATABASE_URL')\\n\\n# API keys\\nSTRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY')\\nSENDGRID_API_KEY = os.getenv('SENDGRID_API_KEY')\\nAWS_SECRET_KEY = os.getenv('AWS_SECRET_KEY')\\n\\n# Encrypted secrets (for production)\\nENCRYPTION_KEY = os.getenv('ENCRYPTION_KEY')\\n\\n# Helper functions for encrypted secrets\\ndef decrypt_secret(encrypted_secret: str) -> str:\\n    if not ENCRYPTION_KEY:\\n        return encrypted_secret\\n    \\n    f = Fernet(ENCRYPTION_KEY.encode())\\n    return f.decrypt(encrypted_secret.encode()).decode()\\n\\n# Use encrypted secrets in production\\nif os.getenv('ENVIRONMENT') == 'production':\\n    ENCRYPTED_DB_PASSWORD = os.getenv('ENCRYPTED_DB_PASSWORD')\\n    if ENCRYPTED_DB_PASSWORD:\\n        DATABASE_PASSWORD = decrypt_secret(ENCRYPTED_DB_PASSWORD)\\n        # Update DATABASE_URL with decrypted password\\n        DATABASE_URL = DATABASE_URL.replace('PASSWORD_PLACEHOLDER', DATABASE_PASSWORD)\\n\\n# Access in your code\\nfrom panther.configs import config\\n\\n@API()\\nasync def payment_api():\\n    stripe_key = config.STRIPE_SECRET_KEY\\n    # Use stripe_key for payment processing\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I optimize performance in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use caching, database optimization, and async patterns. Example:\\n```python\\n# 1. Enable caching for frequently accessed data\\n@API(cache=timedelta(minutes=10))\\nasync def get_popular_posts():\\n    return await Post.find().sort('views', -1).limit(10)\\n\\n# 2. Use database indexes\\nclass Post(Model):\\n    title: str = Field(index=True)\\n    author_id: str = Field(index=True)\\n    created_at: datetime = Field(index=True)\\n\\n# 3. Optimize queries with projection\\nasync def get_user_list():\\n    # Only fetch needed fields\\n    users = await User.find(\\n        {'is_active': True},\\n        projection={'username': 1, 'email': 1, '_id': 0}\\n    )\\n    return users\\n\\n# 4. Use background tasks for heavy operations\\nfrom panther.background_tasks import BackgroundTask\\n\\n@API()\\nasync def create_report():\\n    # Start background task\\n    BackgroundTask(generate_report).submit()\\n    return {'message': 'Report generation started'}\\n\\n# 5. Implement connection pooling\\nDATABASE_CONFIG = {\\n    'max_pool_size': 10,\\n    'min_pool_size': 2,\\n    'max_idle_time': 30000\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I implement monitoring and logging in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use built-in logging and custom monitoring. Example:\\n```python\\nimport logging\\nfrom panther.middlewares import HTTPMiddleware\\nfrom datetime import datetime\\nimport time\\n\\n# Configure logging\\nlogging.basicConfig(\\n    level=logging.INFO,\\n    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',\\n    handlers=[\\n        logging.FileHandler('app.log'),\\n        logging.StreamHandler()\\n    ]\\n)\\n\\nlogger = logging.getLogger(__name__)\\n\\n# Custom middleware for monitoring\\nclass MonitoringMiddleware(HTTPMiddleware):\\n    async def __call__(self, request):\\n        start_time = time.time()\\n        \\n        # Log request\\n        logger.info(f'Request: {request.method} {request.path}')\\n        \\n        try:\\n            response = await self.dispatch(request=request)\\n            \\n            # Log response\\n            duration = time.time() - start_time\\n            logger.info(f'Response: {response.status_code} - {duration:.3f}s')\\n            \\n            return response\\n        except Exception as e:\\n            # Log errors\\n            logger.error(f'Error: {str(e)}')\\n            raise\\n\\n# Custom monitoring in APIs\\n@API()\\nasync def monitored_api(request: Request):\\n    logger.info(f'API called by user: {request.user.username if request.user else \\\"anonymous\\\"}')\\n    \\n    # Track performance\\n    start_time = time.time()\\n    result = await heavy_operation()\\n    duration = time.time() - start_time\\n    \\n    logger.info(f'Heavy operation completed in {duration:.3f}s')\\n    return result\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I implement security best practices in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use input validation, authentication, and security headers. Example:\\n```python\\nfrom panther.app import API\\nfrom panther.request import Request\\nfrom panther.response import Response\\nimport re\\n\\n# 1. Input sanitization\\n@API()\\nasync def secure_api(request: Request):\\n    user_input = request.data.get('comment', '')\\n    \\n    # Sanitize HTML\\n    sanitized = re.sub(r'<[^>]*>', '', user_input)\\n    \\n    # Validate length\\n    if len(sanitized) > 1000:\\n        raise ValueError('Comment too long')\\n    \\n    return {'comment': sanitized}\\n\\n# 2. Rate limiting for sensitive endpoints\\n@API(throttling=Throttle(rate=5, duration=timedelta(minutes=1)))\\nasync def login_api(request: Request):\\n    # Login logic\\n    pass\\n\\n# 3. Security headers middleware\\nclass SecurityHeadersMiddleware(HTTPMiddleware):\\n    async def __call__(self, request):\\n        response = await self.dispatch(request=request)\\n        \\n        # Add security headers\\n        response.headers.update({\\n            'X-Content-Type-Options': 'nosniff',\\n            'X-Frame-Options': 'DENY',\\n            'X-XSS-Protection': '1; mode=block',\\n            'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',\\n            'Content-Security-Policy': \\\"default-src 'self'\\\"\\n        })\\n        \\n        return response\\n\\n# 4. SQL injection prevention (automatic with Panther)\\n@API()\\nasync def safe_query(request: Request):\\n    # Panther automatically prevents SQL injection\\n    user_id = request.query_params.get('user_id')\\n    user = await User.find_one({'id': user_id})\\n    return user\\n\\n# 5. XSS protection\\n@API()\\nasync def safe_output(request: Request):\\n    user_data = request.data\\n    \\n    # Escape output\\n    safe_data = {\\n        'username': html.escape(user_data.get('username', '')),\\n        'email': user_data.get('email', '')\\n    }\\n    \\n    return safe_data\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create custom database backends in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Implement custom database backends by extending Panther's database interface. Example:\\n```python\\nfrom panther.db import BaseDatabase\\nfrom typing import Dict, List, Any\\n\\nclass CustomDatabase(BaseDatabase):\\n    def __init__(self, connection_string: str):\\n        self.connection_string = connection_string\\n        self.connection = None\\n    \\n    async def connect(self):\\n        # Initialize your custom database connection\\n        self.connection = await create_connection(self.connection_string)\\n    \\n    async def disconnect(self):\\n        if self.connection:\\n            await self.connection.close()\\n    \\n    async def insert_one(self, collection: str, data: Dict) -> Dict:\\n        # Custom insert implementation\\n        result = await self.connection.execute(\\n            f'INSERT INTO {collection} VALUES (:data)',\\n            {'data': json.dumps(data)}\\n        )\\n        return {'id': result.lastrowid, **data}\\n    \\n    async def find(self, collection: str, filter_dict: Dict = None) -> List[Dict]:\\n        # Custom find implementation\\n        query = f'SELECT * FROM {collection}'\\n        if filter_dict:\\n            where_clause = self._build_where_clause(filter_dict)\\n            query += f' WHERE {where_clause}'\\n        \\n        results = await self.connection.fetch(query)\\n        return [json.loads(row['data']) for row in results]\\n    \\n    def _build_where_clause(self, filter_dict: Dict) -> str:\\n        # Convert Panther filters to SQL\\n        conditions = []\\n        for key, value in filter_dict.items():\\n            if isinstance(value, dict):\\n                # Handle operators like $gt, $lt\\n                for op, val in value.items():\\n                    if op == '$gt':\\n                        conditions.append(f\\\"JSON_EXTRACT(data, '$.{key}') > {val}\\\")\\n                    elif op == '$lt':\\n                        conditions.append(f\\\"JSON_EXTRACT(data, '$.{key}') < {val}\\\")\\n            else:\\n                conditions.append(f\\\"JSON_EXTRACT(data, '$.{key}') = '{value}'\\\")\\n        \\n        return ' AND '.join(conditions)\\n\\n# Use custom database\\nDATABASE = CustomDatabase('sqlite:///custom.db')\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I create custom validators in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Create custom validators for complex validation logic. Example:\\n```python\\nfrom pydantic import validator, ValidationError\\nimport re\\n\\n# Custom validators\\nclass CustomValidators:\\n    @staticmethod\\n    def validate_phone_number(phone: str) -> str:\\n        # Remove all non-digit characters\\n        digits = re.sub(r'\\\\D', '', phone)\\n        \\n        if len(digits) < 10 or len(digits) > 15:\\n            raise ValueError('Invalid phone number length')\\n        \\n        return digits\\n    \\n    @staticmethod\\n    def validate_password_strength(password: str) -> str:\\n        if len(password) < 8:\\n            raise ValueError('Password must be at least 8 characters')\\n        \\n        if not re.search(r'[A-Z]', password):\\n            raise ValueError('Password must contain uppercase letter')\\n        \\n        if not re.search(r'[a-z]', password):\\n            raise ValueError('Password must contain lowercase letter')\\n        \\n        if not re.search(r'\\\\d', password):\\n            raise ValueError('Password must contain number')\\n        \\n        if not re.search(r'[!@#$%^&*]', password):\\n            raise ValueError('Password must contain special character')\\n        \\n        return password\\n    \\n    @staticmethod\\n    def validate_username_availability(username: str, existing_users: list) -> str:\\n        if username in existing_users:\\n            raise ValueError('Username already taken')\\n        return username\\n\\n# Use in serializers\\nclass UserSerializer(ModelSerializer):\\n    class Config:\\n        model = User\\n        fields = ['username', 'email', 'phone', 'password']\\n    \\n    @validator('phone')\\n    def validate_phone(cls, v):\\n        return CustomValidators.validate_phone_number(v)\\n    \\n    @validator('password')\\n    def validate_password(cls, v):\\n        return CustomValidators.validate_password_strength(v)\\n    \\n    @validator('username')\\n    def validate_username(cls, v):\\n        # Get existing usernames\\n        existing_users = [user.username for user in User.find()]\\n        return CustomValidators.validate_username_availability(v, existing_users)\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"How do I implement advanced routing patterns in Panther?\"}, {\"role\": \"assistant\", \"content\": \"Use dynamic routing and custom route handlers. Example:\\n```python\\n# 1. Dynamic route generation\\nfrom panther.app import API\\nfrom panther.request import Request\\n\\n# Generate routes dynamically\\ndef create_dynamic_routes():\\n    routes = {}\\n    \\n    # Create CRUD routes for each model\\n    models = [User, Post, Category]\\n    \\n    for model in models:\\n        model_name = model.__name__.lower()\\n        \\n        # List route\\n        routes[f'{model_name}/'] = create_list_api(model)\\n        \\n        # Detail route\\n        routes[f'{model_name}/<id>/'] = create_detail_api(model)\\n        \\n        # Custom routes\\n        if hasattr(model, 'custom_routes'):\\n            for route_path, handler in model.custom_routes.items():\\n                routes[f'{model_name}/{route_path}'] = handler\\n    \\n    return routes\\n\\n# 2. Custom route handlers\\n@API()\\nasync def create_list_api(model_class):\\n    async def list_handler(request: Request):\\n        items = await model_class.find()\\n        return items\\n    return list_handler\\n\\n@API()\\nasync def create_detail_api(model_class):\\n    async def detail_handler(request: Request, id: str):\\n        item = await model_class.find_one({'id': id})\\n        if not item:\\n            raise NotFoundAPIError(f'{model_class.__name__} not found')\\n        return item\\n    return detail_handler\\n\\n# 3. Route middleware\\nclass RouteMiddleware(HTTPMiddleware):\\n    async def __call__(self, request):\\n        # Add route information to request\\n        request.route_info = {\\n            'path': request.path,\\n            'method': request.method,\\n            'timestamp': datetime.now()\\n        }\\n        \\n        response = await self.dispatch(request=request)\\n        return response\\n\\n# 4. Conditional routing\\nurls = {\\n    'api/v1/': v1_urls if settings.API_VERSION == 'v1' else v2_urls,\\n    'api/v2/': v2_urls,\\n    'admin/': admin_urls if settings.ENABLE_ADMIN else None,\\n}\\n\\n# 5. Route aliases with different handlers\\nurls = {\\n    'user/': user_api,\\n    'users/': user_api,  # Alias\\n    'u/': user_api,      # Short alias\\n    'profile/': user_profile_api,  # Different handler for same data\\n}\\n```\"}]}\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are learning a new Python backend framework called Panther. I will teach you each component step by step.\"}, {\"role\": \"user\", \"content\": \"What are the best practices for Panther development?\"}, {\"role\": \"assistant\", \"content\": \"Follow these best practices for Panther development:\\n\\n1. **Project Structure**:\\n   - Organize by feature, not by type\\n   - Use consistent naming conventions\\n   - Separate concerns (models, APIs, serializers)\\n\\n2. **Database Design**:\\n   - Use proper indexes for frequently queried fields\\n   - Implement soft deletes for important data\\n   - Use transactions for related operations\\n   - Optimize queries with projections\\n\\n3. **API Design**:\\n   - Use RESTful conventions\\n   - Implement proper error handling\\n   - Use appropriate HTTP status codes\\n   - Add comprehensive validation\\n   - Document with OpenAPI\\n\\n4. **Security**:\\n   - Always validate and sanitize input\\n   - Use authentication and authorization\\n   - Implement rate limiting\\n   - Add security headers\\n   - Keep dependencies updated\\n\\n5. **Performance**:\\n   - Use caching for expensive operations\\n   - Implement pagination for large datasets\\n   - Use background tasks for heavy operations\\n   - Monitor and log performance metrics\\n\\n6. **Testing**:\\n   - Write unit tests for all APIs\\n   - Use fixtures for test data\\n   - Test error scenarios\\n   - Implement integration tests\\n\\n7. **Configuration**:\\n   - Use environment-specific configs\\n   - Validate configuration on startup\\n   - Secure sensitive data\\n   - Use feature flags for gradual rollouts\\n\\n8. **Code Quality**:\\n   - Use type hints consistently\\n   - Follow PEP 8 style guidelines\\n   - Add docstrings to complex functions\\n   - Use meaningful variable names\\n   - Keep functions focused and small\\n\\n9. **Monitoring**:\\n   - Implement comprehensive logging\\n   - Monitor application health\\n   - Track performance metrics\\n   - Set up error alerting\\n\\n10. **Deployment**:\\n    - Use containerization (Docker)\\n    - Implement CI/CD pipelines\\n    - Use environment variables for config\\n    - Set up proper backup strategies\\n    - Monitor production performance\"}]}\n"
  },
  {
    "path": "examples/blog/Dockerfile",
    "content": "# --- Builder ---\nFROM python:3.12 AS builder\nWORKDIR /app\n\n# Create virtual environment\nRUN python -m venv /opt/venv\n\n# Install uv into system (can be reused)\nRUN pip install --no-cache-dir uv\n\n# Copy requirements and install into the venv\nCOPY requirements.txt .\nRUN /usr/local/bin/uv pip install -r requirements.txt --python /opt/venv/bin/python\n\n# --- Final image ---\nFROM python:3.12-slim AS production\n\nENV PYTHONUNBUFFERED=1\nENV PATH=\"/opt/venv/bin:$PATH\"\n\n# Copy venv from builder\nCOPY --from=builder /opt/venv /opt/venv\n\n# Copy app source\nWORKDIR /app\nCOPY . /app\n"
  },
  {
    "path": "examples/blog/Readme.md",
    "content": "A blog with Post and Comment. Post has title and content. Comment belongs to a Post. Anonymous users can view posts. Only authenticated users can create comments.\n"
  },
  {
    "path": "examples/blog/app/apis.py",
    "content": "from datetime import timedelta\n\nfrom panther import status\nfrom panther.app import GenericAPI\nfrom panther.db import Model\nfrom panther.exceptions import BadRequestAPIError\nfrom panther.generics import CreateAPI, DeleteAPI, ListAPI, RetrieveAPI, UpdateAPI\nfrom panther.pagination import Pagination\nfrom panther.permissions import IsAuthenticated, IsAuthenticatedOrReadonly\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.throttling import Throttle\n\nfrom app.models import Post\nfrom app.serializers import CommentSerializer, PostDetailOutputSerializer, PostOutputSerializer, PostSerializer\n\n\n# PostAPI - For Listing Posts and Creating a Post\nclass PostAPI(ListAPI, CreateAPI):\n    permissions = [IsAuthenticatedOrReadonly]\n    input_model = PostSerializer\n    throttling = Throttle(rate=5, duration=timedelta(minutes=1))\n    pagination = Pagination\n    output_model = PostOutputSerializer\n    cache = timedelta(minutes=1)\n\n    async def get_query(self, request: Request, **kwargs):\n        return await Post.find()\n\n\n# PostDetailAPI - For Retrieving, Updating, and Deleting a Post\nclass PostDetailAPI(UpdateAPI, RetrieveAPI, DeleteAPI):\n    input_model = PostSerializer\n    output_model = PostDetailOutputSerializer\n\n    async def get_instance(self, request: Request, **kwargs) -> Model:\n        return await Post.find_one_or_raise(id=kwargs['post_id'])\n\n\nclass CommentAPI(GenericAPI):\n    input_model = CommentSerializer\n    permissions = IsAuthenticated\n\n    async def post(self, request: Request, post_id: str):\n        if not await Post.exists(id=post_id):\n            raise BadRequestAPIError('Post with this ID does not exists.')\n        instance = await request.validated_data.model.insert_one(request.validated_data, post_id=post_id)\n        return Response(data=instance, status_code=status.HTTP_201_CREATED)\n"
  },
  {
    "path": "examples/blog/app/models.py",
    "content": "from panther.db import Model\n\n\nclass Post(Model):\n    title: str\n    content: str\n\n\nclass Comment(Model):\n    content: str\n    post_id: Post  # Foreign Key (relating to Post)\n"
  },
  {
    "path": "examples/blog/app/serializers.py",
    "content": "from typing import Any\n\nfrom app.models import Comment, Post\n\nfrom panther.serializer import ModelSerializer\n\n\nclass PostSerializer(ModelSerializer):\n    class Config:\n        model = Post\n        fields = ['title', 'content']\n\n\nclass PostOutputSerializer(ModelSerializer):\n    class Config:\n        model = Post\n        fields = ['id', 'title', 'content']\n\n\nclass PostDetailOutputSerializer(ModelSerializer):\n    class Config:\n        model = Post\n        fields = ['id', 'title', 'content']\n\n    async def to_response(self, instance: Any, data: dict) -> dict:\n        data['comments'] = await Comment.find(post_id=instance.id)\n        return data\n\n\nclass CommentSerializer(ModelSerializer):\n    class Config:\n        model = Comment\n        fields = ['content']\n"
  },
  {
    "path": "examples/blog/app/urls.py",
    "content": "from app.apis import CommentAPI, PostAPI, PostDetailAPI\n\nurl_routing = {\n    'posts': PostAPI,\n    'comments/<post_id>': CommentAPI,\n    'posts/<post_id>': PostDetailAPI,\n}\n"
  },
  {
    "path": "examples/blog/core/configs.py",
    "content": "import os\nfrom datetime import timedelta\n\nfrom panther.throttling import Throttle\n\n# Generate Secret Key with `panther.utils.generate_secret_key`\nSECRET_KEY = os.environ.get('SECRET_KEY', 'nggtD7uISmU3t61KUpOR642L2MFLUxxY3uoHGfMBH6E=')\n\n# Database Configuration\nDATABASE = {\n    'engine': {\n        'class': 'panther.db.connections.PantherDBConnection',\n        'path': 'database.pdb',\n    }\n}\n\n# Enable Redis for caching and throttling\nREDIS = {\n    'class': 'panther.db.connections.RedisConnection',\n    'host': os.environ.get('REDIS_HOST', '127.0.0.1'),\n    'port': 6379,\n    'db': 0,\n}\n\n# Enable JWT authentication\nAUTHENTICATION = 'panther.authentications.JWTAuthentication'\nJWT_CONFIG = {\n    'algorithm': 'HS256',\n    'life_time': timedelta(days=1),\n    'refresh_life_time': timedelta(days=7),\n}\n\n# Middleware Configuration (CORS)\nALLOW_ORIGINS = ['*']\nALLOW_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']\nALLOW_HEADERS = ['*']\nALLOW_CREDENTIALS = True\nCORS_MAX_AGE = 3600\n\nMIDDLEWARES = [\n    'panther.middlewares.cors.CORSMiddleware',\n    'panther.middlewares.monitoring.MonitoringMiddleware',\n]\n\n# Throttling Configuration\nTHROTTLING = Throttle(rate=5, duration=timedelta(minutes=1))\n\nURLs = 'core.urls.url_routing'\n"
  },
  {
    "path": "examples/blog/core/urls.py",
    "content": "from app.urls import url_routing as app_url_routing\nfrom user.urls import url_routing as user_url_routing\n\nfrom panther.openapi.views import ScalarOpenAPI\nfrom panther.panel.urls import url_routing as panel_url_routing\n\nurl_routing = {\n    'admin/': panel_url_routing,\n    'docs/': ScalarOpenAPI,\n    'user/': user_url_routing,\n    '/': app_url_routing,\n}\n"
  },
  {
    "path": "examples/blog/docker-compose.yaml",
    "content": "volumes:\n  redis_volume:\n\nservices:\n  application:\n    build: .\n    restart: always\n    volumes:\n      - ./:/app\n    ports:\n      - \"8000:8000\"\n    environment:\n      REDIS_HOST: redis\n    depends_on:\n      redis:\n        condition: service_healthy\n    command: [ \"uvicorn\", \"main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\" ]\n\n  redis:\n    image: redis:7.4-bookworm\n    restart: always\n    volumes:\n      - redis_volume:/data\n    healthcheck:\n      test: [ \"CMD\", \"redis-cli\", \"ping\" ]\n      interval: 10s\n      timeout: 3s\n      retries: 3\n      start_period: 5s\n"
  },
  {
    "path": "examples/blog/main.py",
    "content": "from panther import Panther\n\napp = Panther(__name__)\n"
  },
  {
    "path": "examples/blog/requirements.txt",
    "content": "panther==5.0.0b8\nredis==6.2.0\npython-jose==3.5.0\nuvloop==0.21.0"
  },
  {
    "path": "examples/blog/user/apis.py",
    "content": "from user.serializers import UserOutputSerializer, UserSerializer\n\nfrom panther import status\nfrom panther.app import GenericAPI\nfrom panther.db.models import BaseUser\nfrom panther.exceptions import BadRequestAPIError\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.utils import timezone_now\n\n\nclass RegisterAPI(GenericAPI):\n    input_model = UserSerializer\n    output_model = UserOutputSerializer\n\n    async def post(self, request: Request, **kwargs):\n        if await BaseUser.exists(username=request.validated_data.username):\n            raise BadRequestAPIError(detail='User with this username already exists.')\n        instance = await BaseUser.insert_one(\n            username=request.validated_data.username,\n            date_created=timezone_now(),\n        )\n        await instance.set_password(request.validated_data.password)\n        return Response(data=instance, status_code=status.HTTP_201_CREATED)\n\n\nclass LoginAPI(GenericAPI):\n    input_model = UserSerializer\n\n    async def post(self, request: Request):\n        user = await BaseUser.find_one_or_raise(username=request.validated_data.username)\n        if not user.check_password(request.validated_data.password):\n            raise BadRequestAPIError(detail={'detail': 'username or password was wrong.'})\n        tokens = await user.login()\n        return Response(data=tokens, status_code=status.HTTP_200_OK)\n"
  },
  {
    "path": "examples/blog/user/serializers.py",
    "content": "from panther.db.models import BaseUser\nfrom panther.serializer import ModelSerializer\n\n\nclass UserSerializer(ModelSerializer):\n    class Config:\n        model = BaseUser\n        fields = ['username', 'password']\n\n\nclass UserOutputSerializer(ModelSerializer):\n    class Config:\n        model = BaseUser\n        fields = '*'\n        exclude = ['password']\n"
  },
  {
    "path": "examples/blog/user/urls.py",
    "content": "from user.apis import LoginAPI, RegisterAPI\n\nurl_routing = {'register': RegisterAPI, 'login': LoginAPI}\n"
  },
  {
    "path": "examples/broadcast_websocket.py",
    "content": "from panther import Panther\nfrom panther.app import GenericAPI\nfrom panther.db import Model\nfrom panther.response import HTMLResponse\nfrom panther.websocket import GenericWebsocket, send_message_to_websocket\n\n\nclass User(Model):\n    connection_id: str\n\n\nDATABASE = {\n    'engine': {\n        'class': 'panther.db.connections.PantherDBConnection',\n        'path': 'database.pdb',\n    }\n}\n\n\nclass BroadcastWebsocket(GenericWebsocket):\n    async def connect(self, **kwargs):\n        await self.accept()\n        await User.insert_one(connection_id=self.connection_id)\n\n    async def receive(self, data: str | bytes):\n        users = await User.find()\n        for user in users:\n            await send_message_to_websocket(user.connection_id, data)\n\n\nclass MainPage(GenericAPI):\n    def get(self):\n        template = \"\"\"\n        <input id=\"msg\">\n        <button onclick=\"sendMessage()\">Send</button>\n        <ul id=\"log\"></ul>\n        \n        <script>\n            const s = new WebSocket('ws://127.0.0.1:8000/ws');\n            const log = document.getElementById(\"log\");\n            const msgInput = document.getElementById(\"msg\");\n        \n            function sendMessage() {\n                const message = msgInput.value;\n                s.send(message);\n                log.innerHTML += `<li><b>→</b> ${message}</li>`;\n                msgInput.value = '';\n            }\n        \n            s.onmessage = e => {\n                log.innerHTML += `<li><b>←</b> ${e.data}</li>`;\n            };\n        </script>\n        \"\"\"\n        return HTMLResponse(template)\n\n\nurl_routing = {\n    '': MainPage,\n    'ws': BroadcastWebsocket,\n}\napp = Panther(__name__, configs=__name__, urls=url_routing)\n"
  },
  {
    "path": "examples/file_upload_example.py",
    "content": "\"\"\"\nFile Upload Example for Panther\n\nThis example demonstrates how to handle file uploads in Panther using\nthe File and Image classes with proper validation and error handling.\n\"\"\"\n\nfrom datetime import datetime\n\nfrom panther import Panther, status\nfrom panther.app import API\nfrom panther.db import Model\nfrom panther.exceptions import APIError\nfrom panther.file_handler import File, Image\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.serializer import ModelSerializer\n\n# Database configuration\nDATABASE = {\n    'engine': {\n        'class': 'panther.db.connections.PantherDBConnection',\n        'path': 'file_upload_example.pdb',\n    }\n}\n\n\n# Models\nclass Document(Model):\n    title: str\n    file: File\n    uploaded_at: datetime\n    description: str | None = None\n\n\nclass Profile(Model):\n    name: str\n    avatar: Image\n    bio: str | None = None\n\n\n# Serializers\nclass DocumentUploadSerializer(ModelSerializer):\n    class Config:\n        model = Document\n        fields = ['title', 'file', 'description']\n        required_fields = ['title', 'file']\n\n\nclass ProfileUploadSerializer(ModelSerializer):\n    class Config:\n        model = Profile\n        fields = ['name', 'avatar', 'bio']\n        required_fields = ['name', 'avatar']\n\n\n# APIs\n@API(input_model=DocumentUploadSerializer)\nasync def upload_document(request: Request):\n    \"\"\"Upload a document with validation\"\"\"\n    try:\n        file_data = request.validated_data\n        file = file_data.file\n\n        # Additional validation\n        if file.size > 10 * 1024 * 1024:  # 10MB limit\n            raise APIError(\n                detail='File too large. Maximum size is 10MB.', status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE\n            )\n\n        # Check file type\n        allowed_types = ['application/pdf', 'text/plain', 'application/msword']\n        if file.content_type not in allowed_types:\n            raise APIError(\n                detail='File type not allowed. Only PDF, text, and Word documents are accepted.',\n                status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,\n            )\n\n        # Save file to disk\n        saved_path = file.save('uploads/documents/')\n\n        # Store in database\n        document = await Document.insert_one(\n            {\n                'title': file_data.title,\n                'file': file,\n                'uploaded_at': datetime.now(),\n                'description': file_data.description,\n            }\n        )\n\n        return Response(\n            data={\n                'message': 'Document uploaded successfully',\n                'document_id': document.id,\n                'file_name': file.file_name,\n                'file_size': file.size,\n                'saved_path': saved_path,\n            },\n            status_code=status.HTTP_201_CREATED,\n        )\n\n    except Exception as e:\n        raise APIError(detail=f'File upload failed: {str(e)}', status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)\n\n\n@API(input_model=ProfileUploadSerializer)\nasync def upload_profile_image(request: Request):\n    \"\"\"Upload a profile image with automatic image validation\"\"\"\n    try:\n        profile_data = request.validated_data\n        image = profile_data.avatar\n\n        # Image-specific validation\n        if image.size > 5 * 1024 * 1024:  # 5MB limit for images\n            raise APIError(\n                detail='Image too large. Maximum size is 5MB.', status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE\n            )\n\n        # Save image\n        saved_path = image.save('uploads/images/')\n\n        # Store in database\n        profile = await Profile.insert_one({'name': profile_data.name, 'avatar': image, 'bio': profile_data.bio})\n\n        return Response(\n            data={\n                'message': 'Profile image uploaded successfully',\n                'profile_id': profile.id,\n                'image_name': image.file_name,\n                'image_size': image.size,\n                'saved_path': saved_path,\n            },\n            status_code=status.HTTP_201_CREATED,\n        )\n\n    except Exception as e:\n        raise APIError(detail=f'Image upload failed: {str(e)}', status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)\n\n\n@API()\nasync def list_documents(request: Request):\n    \"\"\"List all uploaded documents\"\"\"\n    documents = await Document.find()\n\n    # Convert to response format\n    doc_list = []\n    for doc in documents:\n        doc_list.append(\n            {\n                'id': doc.id,\n                'title': doc.title,\n                'file_name': doc.file.file_name,\n                'file_size': doc.file.size,\n                'uploaded_at': doc.uploaded_at.isoformat(),\n                'description': doc.description,\n            }\n        )\n\n    return Response(data=doc_list)\n\n\n@API()\nasync def get_document(document_id: str):\n    \"\"\"Get a specific document by ID\"\"\"\n    document = await Document.find_one_or_raise(document_id)\n\n    return Response(\n        data={\n            'id': document.id,\n            'title': document.title,\n            'file_name': document.file.file_name,\n            'file_size': document.file.size,\n            'content_type': document.file.content_type,\n            'uploaded_at': document.uploaded_at.isoformat(),\n            'description': document.description,\n        }\n    )\n\n\n# URL routing\nurls = {\n    'upload/document/': upload_document,\n    'upload/profile/': upload_profile_image,\n    'documents/': list_documents,\n    'documents/<document_id>/': get_document,\n}\n\n# Create Panther app\napp = Panther(__name__, configs=__name__, urls=urls)\n"
  },
  {
    "path": "examples/streaming_response.py",
    "content": "import json\nfrom datetime import datetime\nfrom time import sleep\n\nfrom panther import Panther\nfrom panther.app import GenericAPI\nfrom panther.response import StreamingResponse\n\n\nclass MainPage(GenericAPI):\n    def generator(self):\n        \"\"\"Simple number generator with delays\"\"\"\n        for i in range(10):\n            sleep(1)\n            yield f'data: {i}\\n\\n'\n\n    def get(self):\n        return StreamingResponse(data=self.generator())\n\n\nclass ProgressStream(GenericAPI):\n    def generator(self):\n        \"\"\"Simulate a long-running task with progress updates\"\"\"\n        total_steps = 20\n        for i in range(total_steps + 1):\n            progress = (i / total_steps) * 100\n            data = {\n                'step': i,\n                'total_steps': total_steps,\n                'progress': round(progress, 1),\n                'status': 'processing' if i < total_steps else 'completed',\n                'timestamp': datetime.now().isoformat(),\n            }\n            yield f'data: {json.dumps(data)}\\n\\n'\n            sleep(0.5)\n\n    def get(self):\n        return StreamingResponse(data=self.generator())\n\n\nclass LogStream(GenericAPI):\n    def generator(self):\n        \"\"\"Simulate real-time log streaming\"\"\"\n        log_levels = ['INFO', 'WARNING', 'ERROR', 'DEBUG']\n        messages = [\n            'Server started successfully',\n            'Processing request from client',\n            'Database connection established',\n            'Cache miss, fetching from database',\n            'User authentication successful',\n            'File upload completed',\n            'API rate limit approaching',\n            'Memory usage: 45%',\n            'Backup job started',\n            'Task completed successfully',\n        ]\n\n        for i, message in enumerate(messages):\n            level = log_levels[i % len(log_levels)]\n            log_entry = {'timestamp': datetime.now().isoformat(), 'level': level, 'message': message, 'id': i + 1}\n            yield f'data: {json.dumps(log_entry)}\\n\\n'\n            sleep(0.8)\n\n    def get(self):\n        return StreamingResponse(data=self.generator())\n\n\nclass ChatStream(GenericAPI):\n    def generator(self):\n        \"\"\"Simulate a chat bot response\"\"\"\n        response_parts = [\n            \"Hello! I'm a streaming chatbot.\",\n            'I can help you with various tasks.',\n            'For example, I can answer questions,',\n            'provide information, or just chat!',\n            'What would you like to know today?',\n        ]\n\n        for i, part in enumerate(response_parts):\n            data = {\n                'type': 'message',\n                'content': part,\n                'part': i + 1,\n                'total_parts': len(response_parts),\n                'timestamp': datetime.now().isoformat(),\n            }\n            yield f'data: {json.dumps(data)}\\n\\n'\n            sleep(0.6)\n\n    def get(self):\n        return StreamingResponse(data=self.generator())\n\n\nclass ErrorHandlingStream(GenericAPI):\n    def generator(self):\n        \"\"\"Demonstrate error handling in streaming\"\"\"\n        try:\n            for i in range(5):\n                if i == 3:\n                    # Simulate an error\n                    raise Exception('Simulated error during streaming')\n\n                data = {'step': i, 'status': 'success', 'message': f'Processed step {i}'}\n                yield f'data: {json.dumps(data)}\\n\\n'\n                sleep(1)\n        except Exception as e:\n            error_data = {'error': True, 'message': str(e), 'timestamp': datetime.now().isoformat()}\n            yield f'data: {json.dumps(error_data)}\\n\\n'\n\n    def get(self):\n        return StreamingResponse(data=self.generator())\n\n\nurl_routing = {\n    '': MainPage,\n    'progress': ProgressStream,\n    'logs': LogStream,\n    'chat': ChatStream,\n    'error-demo': ErrorHandlingStream,\n}\napp = Panther(__name__, configs=__name__, urls=url_routing)\n"
  },
  {
    "path": "panther/__init__.py",
    "content": "from panther.main import Panther  # noqa: F401\n\n__version__ = '5.2.3'\n\n\ndef version():\n    return __version__\n"
  },
  {
    "path": "panther/_load_configs.py",
    "content": "import logging\nimport sys\nimport types\nfrom importlib import import_module\nfrom multiprocessing import Manager\n\nimport jinja2\n\nfrom panther._utils import check_class_type_endpoint, check_function_type_endpoint, import_class\nfrom panther.authentications import JWTAuthentication\nfrom panther.background_tasks import _background_tasks\nfrom panther.base_websocket import WebsocketConnections\nfrom panther.cli.utils import import_error\nfrom panther.configs import JWTConfig, config\nfrom panther.db.connections import redis\nfrom panther.db.queries.mongodb_queries import BaseMongoDBQuery\nfrom panther.db.queries.pantherdb_queries import BasePantherDBQuery\nfrom panther.exceptions import PantherError\nfrom panther.middlewares.base import HTTPMiddleware, WebsocketMiddleware\nfrom panther.middlewares.monitoring import MonitoringMiddleware, WebsocketMonitoringMiddleware\nfrom panther.panel.views import HomeView\nfrom panther.routings import finalize_urls, flatten_urls\n\n__all__ = (\n    'check_endpoints_inheritance',\n    'load_authentication_class',\n    'load_auto_reformat',\n    'load_background_tasks',\n    'load_configs_module',\n    'load_database',\n    'load_log_queries',\n    'load_middlewares',\n    'load_other_configs',\n    'load_redis',\n    'load_secret_key',\n    'load_templates_dir',\n    'load_throttling',\n    'load_timezone',\n    'load_urls',\n    'load_user_model',\n    'load_websocket_connections',\n)\n\nlogger = logging.getLogger('panther')\nmonitoring_logger = logging.getLogger('monitoring')\n\n\ndef load_configs_module(module_name: str, /) -> dict:\n    \"\"\"Read the config file as dict\"\"\"\n    if module_name:\n        _module = sys.modules[module_name]\n    else:\n        try:\n            _module = import_module('core.configs')\n        except ModuleNotFoundError:\n            raise _exception_handler(field='core/configs.py', error='Not Found')\n    return _module.__dict__\n\n\ndef load_redis(_configs: dict, /) -> None:\n    if redis_config := _configs.get('REDIS'):\n        # Check redis module installation\n        try:\n            from redis.asyncio import Redis\n        except ImportError as e:\n            raise import_error(e, package='redis')\n        redis_class_path = redis_config.get('class', 'panther.db.connections.RedisConnection')\n        redis_class = import_class(redis_class_path)\n        # We have to create another dict then pop the 'class' else we can't pass the tests\n        args = redis_config.copy()\n        args.pop('class', None)\n        redis_class(**args, init=True)\n\n\ndef load_timezone(_configs: dict, /) -> None:\n    if timezone := _configs.get('TIMEZONE'):\n        config.TIMEZONE = timezone\n\n\ndef load_templates_dir(_configs: dict, /) -> None:\n    if templates_dir := _configs.get('TEMPLATES_DIR', '.'):\n        config.TEMPLATES_DIR = templates_dir\n\n    if config.TEMPLATES_DIR == '.':\n        config.TEMPLATES_DIR = config.BASE_DIR\n\n    config.JINJA_ENVIRONMENT = jinja2.Environment(\n        loader=jinja2.ChoiceLoader(\n            loaders=(\n                jinja2.FileSystemLoader(searchpath=config.TEMPLATES_DIR),\n                jinja2.PackageLoader(package_name='panther', package_path='panel/templates/'),\n                jinja2.PackageLoader(package_name='panther', package_path='openapi/templates/'),\n            ),\n        ),\n    )\n\n\ndef load_database(_configs: dict, /) -> None:\n    database_config = _configs.get('DATABASE', {})\n    if 'engine' in database_config:\n        if 'class' not in database_config['engine']:\n            raise _exception_handler(field='DATABASE', error='`engine[\"class\"]` not found.')\n\n        engine_class_path = database_config['engine']['class']\n        engine_class = import_class(engine_class_path)\n        # We have to create another dict then pop the 'class' else we can't pass the tests\n        args = database_config['engine'].copy()\n        args.pop('class')\n        config.DATABASE = engine_class(**args)\n\n        if engine_class_path == 'panther.db.connections.PantherDBConnection':\n            config.QUERY_ENGINE = BasePantherDBQuery\n        elif engine_class_path == 'panther.db.connections.MongoDBConnection':\n            config.QUERY_ENGINE = BaseMongoDBQuery\n\n    if 'query' in database_config:\n        if config.QUERY_ENGINE:\n            logger.warning('`DATABASE.query` has already been filled.')\n        config.QUERY_ENGINE = import_class(database_config['query'])\n\n\ndef load_secret_key(_configs: dict, /) -> None:\n    if secret_key := _configs.get('SECRET_KEY'):\n        config.SECRET_KEY = secret_key\n\n\ndef load_throttling(_configs: dict, /) -> None:\n    if throttling := _configs.get('THROTTLING'):\n        config.THROTTLING = throttling\n\n\ndef load_user_model(_configs: dict, /) -> None:\n    config.USER_MODEL = import_class(_configs.get('USER_MODEL', 'panther.db.models.BaseUser'))\n    if config.USER_MODEL not in config.MODELS:\n        config.MODELS.append(config.USER_MODEL)\n\n\ndef load_log_queries(_configs: dict, /) -> None:\n    if _configs.get('LOG_QUERIES'):\n        config.LOG_QUERIES = True\n\n\ndef load_middlewares(_configs: dict, /) -> None:\n    # Collect HTTP Middlewares\n    for middleware in _configs.get('MIDDLEWARES') or []:\n        # This block is for Backward Compatibility\n        if isinstance(middleware, list | tuple):\n            if len(middleware) == 1:\n                middleware = middleware[0]\n            elif len(middleware) == 2:\n                _deprecated_warning(\n                    field='MIDDLEWARES',\n                    message='`data` does not supported in middlewares anymore, as your data is static you may want '\n                    'to pass them to your middleware with config variables',\n                )\n                middleware = middleware[0]\n            else:\n                raise _exception_handler(\n                    field='MIDDLEWARES',\n                    error=f'{middleware} should be dotted path or type of a middleware class',\n                )\n\n        # `middleware` can be type or path of a class\n        if not callable(middleware):\n            try:\n                middleware = import_class(middleware)\n            except (AttributeError, ModuleNotFoundError):\n                raise _exception_handler(\n                    field='MIDDLEWARES',\n                    error=f'{middleware} is not a valid middleware path or type',\n                )\n\n        if not issubclass(middleware, HTTPMiddleware):\n            raise _exception_handler(field='MIDDLEWARES', error=f'{middleware} is not a sub class of `HTTPMiddleware`')\n\n        if issubclass(middleware, MonitoringMiddleware):\n            monitoring_logger.debug('')  # Initiated\n            config.MONITORING = True\n\n        config.HTTP_MIDDLEWARES.insert(0, middleware)\n\n    # Collect WebSocket Middlewares\n    for middleware in _configs.get('WS_MIDDLEWARES') or []:\n        # `middleware` can be type or path of a class\n        if not callable(middleware):\n            try:\n                middleware = import_class(middleware)\n            except (AttributeError, ModuleNotFoundError):\n                raise _exception_handler(\n                    field='WS_MIDDLEWARES',\n                    error=f'{middleware} is not a valid middleware path or type',\n                )\n\n        if not issubclass(middleware, WebsocketMiddleware):\n            raise _exception_handler(\n                field='WS_MIDDLEWARES', error=f'{middleware} is not a sub class of `WebsocketMiddleware`'\n            )\n\n        if issubclass(middleware, WebsocketMonitoringMiddleware):\n            monitoring_logger.debug('')  # Initiated\n            config.MONITORING = True\n\n        config.WS_MIDDLEWARES.insert(0, middleware)\n\n\ndef load_auto_reformat(_configs: dict, /) -> None:\n    if _configs.get('AUTO_REFORMAT'):\n        config.AUTO_REFORMAT = True\n\n\ndef load_background_tasks(_configs: dict, /) -> None:\n    if _configs.get('BACKGROUND_TASKS'):\n        config.BACKGROUND_TASKS = True\n        _background_tasks.initialize()\n\n\ndef load_other_configs(_configs: dict, /) -> None:\n    known_configs = set(config.__dataclass_fields__)\n    for key, value in _configs.items():\n        if key.isupper() and key not in known_configs:\n            config[key] = value\n\n\ndef load_urls(_configs: dict, /, urls: dict | None) -> None:\n    \"\"\"\n    Return tuple of all urls (as a flat dict) and (as a nested dict)\n    \"\"\"\n    if isinstance(urls, dict):\n        pass\n\n    elif (url_routing := _configs.get('URLs')) is None:\n        raise _exception_handler(field='URLs', error='required.')\n\n    elif isinstance(url_routing, dict):\n        error = (\n            \"can't be 'dict', you may want to pass it's value directly to Panther(). Example: Panther(..., urls=...)\"\n        )\n        raise _exception_handler(field='URLs', error=error)\n\n    elif not isinstance(url_routing, str):\n        error = 'should be dotted string.'\n        raise _exception_handler(field='URLs', error=error)\n\n    else:\n        try:\n            urls = import_class(url_routing)\n        except ModuleNotFoundError as e:\n            raise _exception_handler(field='URLs', error=e)\n\n        if not isinstance(urls, dict):\n            raise _exception_handler(field='URLs', error='should point to a dict.')\n\n    config.FLAT_URLS = flatten_urls(urls)\n    config.URLS = finalize_urls(config.FLAT_URLS)\n\n\ndef load_authentication_class(_configs: dict, /) -> None:\n    \"\"\"Should be after `load_secret_key()` and `load_urls()`\"\"\"\n    if authentication := _configs.get('AUTHENTICATION'):\n        config.AUTHENTICATION = import_class(authentication)\n\n    if ws_authentication := _configs.get('WS_AUTHENTICATION'):\n        config.WS_AUTHENTICATION = import_class(ws_authentication)\n\n    load_jwt_config(_configs)\n\n\ndef load_jwt_config(_configs: dict, /) -> None:\n    \"\"\"Only Collect JWT Config If Authentication Is JWTAuthentication\"\"\"\n\n    auth_is_jwt = (config.AUTHENTICATION and issubclass(config.AUTHENTICATION, JWTAuthentication)) or (\n        config.WS_AUTHENTICATION and issubclass(config.WS_AUTHENTICATION, JWTAuthentication)\n    )\n\n    jwt_config = _configs.get('JWT_CONFIG', {})\n    using_panel_views = HomeView in config.FLAT_URLS.values()\n    if auth_is_jwt or using_panel_views:\n        if 'key' not in jwt_config:\n            if config.SECRET_KEY is None:\n                raise _exception_handler(field='JWTConfig', error='`JWTConfig.key` or `SECRET_KEY` is required.')\n            jwt_config['key'] = config.SECRET_KEY\n        config.JWT_CONFIG = JWTConfig(**jwt_config)\n\n        try:\n            import jose\n        except ImportError as e:\n            raise import_error(e, package='python-jose')\n\n\ndef load_websocket_connections():\n    \"\"\"Should be after `load_redis()`\"\"\"\n    if config.HAS_WS:\n        # Check `websockets`\n        try:\n            import websockets\n        except ImportError as e:\n            raise import_error(e, package='websockets')\n\n        # Use the redis pubsub if `redis.is_connected`, else use the `multiprocessing.Manager`\n        pubsub_connection = redis.create_connection_for_websocket() if redis.is_connected else Manager()\n        config.WEBSOCKET_CONNECTIONS = WebsocketConnections(pubsub_connection=pubsub_connection)\n\n\ndef check_endpoints_inheritance():\n    \"\"\"Should be after `load_urls()`\"\"\"\n    for endpoint in config.FLAT_URLS.values():\n        if endpoint == {}:\n            continue\n\n        if isinstance(endpoint, types.FunctionType):\n            check_function_type_endpoint(endpoint=endpoint)\n        else:\n            check_class_type_endpoint(endpoint=endpoint)\n\n\ndef _exception_handler(field: str, error: str | Exception) -> PantherError:\n    return PantherError(f\"Invalid '{field}': {error}\")\n\n\ndef _deprecated_warning(field: str, message: str):\n    return logger.warning(f\"DEPRECATED '{field}': {message}\")\n"
  },
  {
    "path": "panther/_utils.py",
    "content": "import asyncio\nimport importlib\nimport inspect\nimport logging\nimport mimetypes\nimport re\nimport subprocess\nimport traceback\nimport types\nfrom collections.abc import AsyncGenerator, Callable, Generator, Iterator\nfrom datetime import timedelta\nfrom pathlib import Path\nfrom traceback import TracebackException\nfrom typing import Any\n\nfrom panther.exceptions import PantherError\nfrom panther.file_handler import File\n\nlogger = logging.getLogger('panther')\n\nENDPOINT_FUNCTION_BASED_API = 0\nENDPOINT_CLASS_BASED_API = 1\nENDPOINT_WEBSOCKET = 2\n\n\ndef import_class(dotted_path: str, /) -> type[Any]:\n    \"\"\"\n    Example:\n    -------\n        Input: panther.db.models.User\n        Output: User (The Class)\n\n    \"\"\"\n    path, name = dotted_path.rsplit('.', 1)\n    module = importlib.import_module(path)\n    return getattr(module, name)\n\n\nNEWLINE_CRLF = b'\\r\\n'  # Windows-style\nNEWLINE_LF = b'\\n'  # Unix/Linux-style\n\n# Regex patterns for CRLF (Windows)\nFIELD_PATTERN_CRLF = re.compile(rb'Content-Disposition: form-data; name=\"(.*)\"\\r\\n\\r\\n(.*)', flags=re.DOTALL)\nFILE_PATTERN_CRLF = re.compile(rb'Content-Disposition: form-data; name=\"(.*)\"; filename=\"(.*)\"\\r\\nContent-Type: (.*)')\n\n# Regex patterns for LF (Linux)\nFIELD_PATTERN_LF = re.compile(rb'Content-Disposition: form-data; name=\"(.*)\"\\n\\n(.*)', flags=re.DOTALL)\nFILE_PATTERN_LF = re.compile(rb'Content-Disposition: form-data; name=\"(.*)\"; filename=\"(.*)\"\\nContent-Type: (.*)')\n\n\ndef read_multipart_form_data(boundary: str, body: bytes) -> dict:\n    boundary_bytes = b'--' + boundary.encode()\n\n    # Choose newline type and corresponding patterns\n    if body.endswith(NEWLINE_CRLF):\n        newline = NEWLINE_CRLF\n        field_pattern = FIELD_PATTERN_CRLF\n        file_pattern = FILE_PATTERN_CRLF\n    else:\n        newline = NEWLINE_LF\n        field_pattern = FIELD_PATTERN_LF\n        file_pattern = FILE_PATTERN_LF\n\n    data = {}\n    for part in body.split(boundary_bytes):\n        part = part.removeprefix(newline).removesuffix(newline)\n\n        if part in (b'', b'--'):\n            continue\n\n        if match := field_pattern.match(string=part):\n            field_name, value = match.groups()\n            data[field_name.decode('utf-8')] = value.decode('utf-8')\n            continue\n\n        try:\n            headers, file_content = part.split(2 * newline, 1)\n        except ValueError:\n            logger.error('Malformed part, skipping.')\n            continue\n\n        if match := file_pattern.match(string=headers):\n            field_name, file_name, content_type = match.groups()\n            data[field_name.decode('utf-8')] = File(\n                file_name=file_name.decode('utf-8'),\n                content_type=content_type.decode('utf-8'),\n                file=file_content,\n            )\n        else:\n            logger.error('Unrecognized multipart format')\n\n    return data\n\n\ndef is_function_async(func: Callable) -> bool:\n    \"\"\"\n    Sync result is 0 --> False\n    async result is 128 --> True\n    \"\"\"\n    return bool(func.__code__.co_flags & (1 << 7))\n\n\ndef traceback_message(exception: Exception) -> str:\n    tb = TracebackException(type(exception), exception, exception.__traceback__)\n    return ''.join(tb.format(chain=False))\n\n\ndef reformat_code(base_dir):\n    try:\n        subprocess.run(['ruff', 'format', base_dir])\n        subprocess.run(['ruff', 'check', '--select', 'I', '--fix', base_dir])\n    except FileNotFoundError:\n        raise PantherError(\"No module named 'ruff', Hint: `pip install ruff`\")\n\n\ndef check_function_type_endpoint(endpoint: types.FunctionType):\n    # Function Doesn't Have @API Decorator\n    if not hasattr(endpoint, '__wrapped__'):\n        raise PantherError(\n            f'You may have forgotten to use `@API()` on the `{endpoint.__module__}.{endpoint.__name__}()`',\n        )\n\n\ndef check_class_type_endpoint(endpoint: Callable):\n    from panther.app import GenericAPI\n    from panther.websocket import GenericWebsocket\n\n    if not issubclass(endpoint, (GenericAPI, GenericWebsocket)):\n        raise PantherError(\n            f'You may have forgotten to inherit from `panther.app.GenericAPI` or `panther.app.GenericWebsocket` '\n            f'on the `{endpoint.__module__}.{endpoint.__name__}()`',\n        )\n\n\ndef async_next(iterator: Iterator):\n    \"\"\"\n    The StopIteration exception is a special case in Python,\n    particularly when it comes to asynchronous programming and the use of asyncio.\n    This is because StopIteration is not meant to be caught in the traditional sense;\n        it's used internally by Python to signal the end of an iteration.\n    \"\"\"\n    try:\n        return next(iterator)\n    except StopIteration:\n        raise StopAsyncIteration\n\n\nasync def to_async_generator(generator: Generator) -> AsyncGenerator:\n    while True:\n        try:\n            yield await asyncio.to_thread(async_next, iter(generator))\n        except StopAsyncIteration:\n            break\n\n\ndef validate_api_auth(auth):\n    \"\"\"Validate the auth callable or class for correct async signature and argument count.\"\"\"\n    if auth is None:\n        return None\n\n    if not callable(auth):\n        msg = (\n            f'`{type(auth).__name__}` is not valid for authentication, it should be a callable, a Class with __call__ '\n            f'method or a single function.'\n        )\n        logger.error(msg)\n        raise PantherError(msg)\n\n    # If it's a class, validate its __call__\n    if inspect.isclass(auth):\n        call_method = getattr(auth, '__call__', None)\n        if not inspect.isfunction(call_method):\n            msg = f'{auth.__name__} must implement __call__() method.'\n            logger.error(msg)\n            raise PantherError(msg)\n        func = call_method\n        expected_args = 2  # self, request\n        func_name = f'{auth.__name__}.__call__()'\n    else:\n        func = auth\n        expected_args = 1  # request\n        func_name = f'{auth.__name__}()'\n\n    sig = inspect.signature(func)\n    if len(sig.parameters) != expected_args:\n        msg = f'{func_name} requires {expected_args} positional argument(s) ({\"self, \" if expected_args == 2 else \"\"}request).'\n        logger.error(msg)\n        raise PantherError(msg)\n\n    # Check if async\n    if not is_function_async(func):\n        msg = f'{func_name} should be `async`'\n        logger.error(msg)\n        raise PantherError(msg)\n\n\ndef validate_api_permissions(permissions):\n    if permissions is None:\n        return permissions\n\n    for perm in permissions:\n        if not callable(perm):\n            msg = (\n                f'`{type(perm).__name__}` is not valid for permission, it should be a callable, a Class with __call__ '\n                f'method or a single function.'\n            )\n            logger.error(msg)\n            raise PantherError(msg)\n\n        # If it's a class, validate its __call__\n        if inspect.isclass(perm):\n            call_method = getattr(perm, '__call__', None)\n            if not inspect.isfunction(call_method):\n                msg = f'{perm.__name__} must implement __call__() method.'\n                logger.error(msg)\n                raise PantherError(msg)\n            func = call_method\n            expected_args = 2  # self, request\n            func_name = f'{perm.__name__}.__call__()'\n        else:\n            func = perm\n            expected_args = 1  # request\n            func_name = f'{perm.__name__}()'\n\n        sig = inspect.signature(func)\n        if len(sig.parameters) != expected_args:\n            msg = f'{func_name} requires {expected_args} positional argument(s) ({\"self, \" if expected_args == 2 else \"\"}request).'\n            logger.error(msg)\n            raise PantherError(msg)\n\n        # Check if async\n        if not is_function_async(func):\n            msg = f'{func_name} should be `async`'\n            logger.error(msg)\n            raise PantherError(msg)\n\n\ndef check_api_deprecations(cache, **kwargs):\n    # Check Cache Usage\n    if kwargs.pop('cache_exp_time', None):\n        deprecation_message = (\n            traceback.format_stack(limit=2)[0]\n            + '\\nThe `cache_exp_time` argument has been removed in Panther v5 and is no longer available.'\n            '\\nYou may want to use `cache` instead.'\n        )\n        raise PantherError(deprecation_message)\n    if cache and not isinstance(cache, timedelta):\n        deprecation_message = (\n            traceback.format_stack(limit=2)[0] + '\\nThe `cache` argument has been changed in Panther v5, '\n            'it should be an instance of `datetime.timedelta()`.'\n        )\n        raise PantherError(deprecation_message)\n    # Check Others\n    if kwargs:\n        msg = f'Unknown kwargs: {kwargs.keys()}'\n        logger.error(msg)\n        raise PantherError(msg)\n\n\nCUSTOM_MIME_TYPES = {\n    # Text & Web\n    'html': 'text/html',\n    'htm': 'text/html',\n    'css': 'text/css',\n    'csv': 'text/csv',\n    'txt': 'text/plain',\n    'md': 'text/markdown',\n    'xml': 'application/xml',\n    'json': 'application/json',\n    'yml': 'text/yaml',\n    'yaml': 'text/yaml',\n    'js': 'application/javascript',\n    'mjs': 'application/javascript',\n    'ts': 'application/typescript',\n    'map': 'application/json',\n    # Images\n    'png': 'image/png',\n    'jpg': 'image/jpeg',\n    'jpeg': 'image/jpeg',\n    'gif': 'image/gif',\n    'bmp': 'image/bmp',\n    'webp': 'image/webp',\n    'svg': 'image/svg+xml',\n    'ico': 'image/x-icon',\n    'tiff': 'image/tiff',\n    'avif': 'image/avif',\n    # Fonts\n    'woff': 'font/woff',\n    'woff2': 'font/woff2',\n    'ttf': 'font/ttf',\n    'otf': 'font/otf',\n    'eot': 'application/vnd.ms-fontobject',\n    # Audio\n    'mp3': 'audio/mpeg',\n    'ogg': 'audio/ogg',\n    'wav': 'audio/wav',\n    'flac': 'audio/flac',\n    'm4a': 'audio/mp4',\n    'aac': 'audio/aac',\n    # Video\n    'mp4': 'video/mp4',\n    'webm': 'video/webm',\n    'ogv': 'video/ogg',\n    'mov': 'video/quicktime',\n    'avi': 'video/x-msvideo',\n    'mkv': 'video/x-matroska',\n    # Archives & binaries\n    'zip': 'application/zip',\n    'gz': 'application/gzip',\n    'tar': 'application/x-tar',\n    'rar': 'application/vnd.rar',\n    '7z': 'application/x-7z-compressed',\n    'bz2': 'application/x-bzip2',\n    # Documents\n    'pdf': 'application/pdf',\n    'rtf': 'application/rtf',\n    'doc': 'application/msword',\n    'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n    'xls': 'application/vnd.ms-excel',\n    'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n    'ppt': 'application/vnd.ms-powerpoint',\n    'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n    # Code & Config\n    'py': 'text/x-python',\n    'go': 'text/x-go',\n    'java': 'text/x-java-source',\n    'c': 'text/x-c',\n    'cpp': 'text/x-c++',\n    'h': 'text/x-c',\n    'sh': 'application/x-sh',\n    'bat': 'application/x-msdos-program',\n    'ini': 'text/plain',\n    'env': 'text/plain',\n}\n\n\ndef detect_mime_type(file_path: str | Path) -> str:\n    file_path = str(file_path)\n\n    # Try extension-based detection\n    mime_type, _ = mimetypes.guess_type(file_path)\n    if mime_type:\n        return mime_type\n\n    # Try content-based detection (magic numbers)\n    try:\n        with open(file_path, 'rb') as f:\n            header = f.read(16)\n            if header.startswith(b'\\x89PNG\\r\\n\\x1a\\n'):\n                return 'image/png'\n            elif header.startswith(b'%PDF'):\n                return 'application/pdf'\n            elif header.startswith(b'PK\\x03\\x04'):\n                return 'application/zip'\n            elif header.startswith(b'\\xff\\xd8\\xff'):\n                return 'image/jpeg'\n            elif header.startswith(b'GIF87a') or header.startswith(b'GIF89a'):\n                return 'image/gif'\n            elif header.startswith(b'BM'):\n                return 'image/bmp'\n            elif header.startswith(b'\\x00\\x00\\x01\\x00'):\n                return 'image/x-icon'  # ICO file\n            elif header.startswith(b'II*\\x00') or header.startswith(b'MM\\x00*'):\n                return 'image/tiff'\n            elif header[:4] == b'\\x00\\x00\\x00\\x18' and b'ftyp' in header:\n                return 'video/mp4'\n    except Exception:\n        pass\n\n    # Custom mapping fallback\n    if (ext := file_path.rsplit('.', maxsplit=1)[1].lower()) in CUSTOM_MIME_TYPES:\n        return CUSTOM_MIME_TYPES[ext]\n\n    # Fallback if no match\n    return 'application/octet-stream'\n"
  },
  {
    "path": "panther/app.py",
    "content": "import functools\nimport inspect\nimport logging\nfrom collections.abc import Callable\nfrom datetime import timedelta\nfrom typing import Literal\n\nfrom pydantic import BaseModel\n\nfrom panther._utils import (\n    ENDPOINT_CLASS_BASED_API,\n    ENDPOINT_FUNCTION_BASED_API,\n    check_api_deprecations,\n    is_function_async,\n    validate_api_auth,\n    validate_api_permissions,\n)\nfrom panther.base_request import BaseRequest\nfrom panther.caching import (\n    get_response_from_cache,\n    set_response_in_cache,\n)\nfrom panther.configs import config\nfrom panther.exceptions import (\n    AuthorizationAPIError,\n    MethodNotAllowedAPIError,\n)\nfrom panther.middlewares import HTTPMiddleware\nfrom panther.openapi import OutputSchema\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.serializer import ModelSerializer\nfrom panther.throttling import Throttle\n\n__all__ = ('API', 'GenericAPI')\n\nlogger = logging.getLogger('panther')\n\n\nclass API:\n    \"\"\"\n    methods: Specify the allowed methods.\n    input_model: The `request.data` will be validated with this attribute, It will raise an\n        `panther.exceptions.BadRequestAPIError` or put the validated data in the `request.validated_data`.\n    output_model: The `response.data` will be passed through this class to filter its attributes.\n    output_schema: This attribute only used in creation of OpenAPI scheme which is available in `panther.openapi.urls`\n        You may want to add its `url` to your urls.\n    auth: It will authenticate the user with header of its request or raise an\n        `panther.exceptions.AuthenticationAPIError`.\n    permissions: List of permissions that will be called sequentially after authentication to authorize the user.\n    throttling: It will limit the users' request on a specific (time-window, path)\n    cache: Specify the duration of the cache (Will be used only in GET requests).\n    middlewares: These middlewares have inner priority than global middlewares.\n    \"\"\"\n\n    func: Callable\n\n    def __init__(\n        self,\n        *,\n        methods: list[Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE']] | None = None,\n        input_model: type[ModelSerializer] | type[BaseModel] | None = None,\n        output_model: type[ModelSerializer] | type[BaseModel] | None = None,\n        output_schema: OutputSchema | None = None,\n        auth: Callable | None = None,\n        permissions: list[Callable] | Callable | None = None,\n        throttling: Throttle | None = None,\n        cache: timedelta | None = None,\n        middlewares: list[type[HTTPMiddleware]] | None = None,\n        **kwargs,\n    ):\n        self.methods = {m.upper() for m in methods} if methods else {'GET', 'POST', 'PUT', 'PATCH', 'DELETE'}\n        self.input_model = input_model\n        self.output_model = output_model\n        self.output_schema = output_schema\n        self.auth = auth\n        self.permissions = permissions\n        if self.permissions is not None and not isinstance(self.permissions, list):\n            self.permissions = [self.permissions]\n        self.throttling = throttling\n        self.cache = cache\n        self.middlewares = middlewares\n        if self.auth is not None:\n            validate_api_auth(self.auth)\n        validate_api_permissions(self.permissions)\n        check_api_deprecations(self.cache, **kwargs)\n\n    def __call__(self, func):\n        self.func = func\n        self.is_function_async = is_function_async(self.func)\n        self.function_annotations = {\n            k: v for k, v in func.__annotations__.items() if v in {BaseRequest, Request, bool, int}\n        }\n\n        @functools.wraps(func)\n        async def wrapper(request: Request) -> Response:\n            chained_func = self.handle_endpoint\n            if self.middlewares:\n                for middleware in reversed(self.middlewares):\n                    chained_func = middleware(chained_func)\n            return await chained_func(request=request)\n\n        # Store attributes on the function, so have the same behaviour as class-based (useful in `openapi.view.OpenAPI`)\n        wrapper.auth = self.auth\n        wrapper.cache = self.cache\n        wrapper.methods = self.methods\n        wrapper.throttling = self.throttling\n        wrapper.permissions = self.permissions\n        wrapper.middlewares = self.middlewares\n        wrapper.input_model = self.input_model\n        wrapper.output_model = self.output_model\n        wrapper.output_schema = self.output_schema\n        wrapper._endpoint_type = ENDPOINT_FUNCTION_BASED_API\n        return wrapper\n\n    async def handle_endpoint(self, request: Request) -> Response:\n        # 1. Check Method\n        if request.method not in self.methods:\n            raise MethodNotAllowedAPIError\n\n        # 2. Authentication\n        if self.auth or config.AUTHENTICATION:\n            auth = self.auth or config.AUTHENTICATION\n            if inspect.isclass(auth):\n                auth = auth()\n            request.user = await auth(request)\n\n        # 3. Permissions\n        if self.permissions:\n            for perm in self.permissions:\n                if inspect.isclass(perm):\n                    perm = perm()\n                if await perm(request) is False:\n                    raise AuthorizationAPIError\n\n        # 4. Throttle\n        if self.throttling or config.THROTTLING:\n            throttling = self.throttling or config.THROTTLING\n            await throttling.check_and_increment(request=request)\n\n        # 5. Validate Input\n        if self.input_model and request.method in {'POST', 'PUT', 'PATCH'}:\n            request.validate_data(model=self.input_model)\n\n        # 6. Get Cached Response\n        if self.cache and request.method == 'GET':\n            if cached := await get_response_from_cache(request=request, duration=self.cache):\n                return Response(data=cached.data, headers=cached.headers, status_code=cached.status_code)\n\n        # 7. Put PathVariables and Request(If User Wants It) In kwargs\n        kwargs = request.clean_parameters(self.function_annotations)\n\n        # 8. Call Endpoint\n        if self.is_function_async:\n            response = await self.func(**kwargs)\n        else:\n            response = self.func(**kwargs)\n\n        # 9. Clean Response\n        if not isinstance(response, Response):\n            response = Response(data=response)\n        if self.output_model and response.data:\n            await response.serialize_output(output_model=self.output_model)\n        if response.pagination:\n            response.data = await response.pagination.template(response.data)\n\n        # 10. Set New Response To Cache\n        if self.cache and request.method == 'GET':\n            await set_response_in_cache(request=request, response=response, duration=self.cache)\n\n        return response\n\n\nclass GenericAPI:\n    \"\"\"\n    Check out the documentation of `panther.app.API()`.\n    \"\"\"\n\n    _endpoint_type = ENDPOINT_CLASS_BASED_API\n\n    input_model: type[ModelSerializer] | type[BaseModel] | None = None\n    output_model: type[ModelSerializer] | type[BaseModel] | None = None\n    output_schema: OutputSchema | None = None\n    auth: Callable | None = None\n    permissions: list[Callable] | Callable | None = None\n    throttling: Throttle | None = None\n    cache: timedelta | None = None\n    middlewares: list[HTTPMiddleware] | None = None\n\n    def __init_subclass__(cls, **kwargs):\n        if cls.permissions is not None and not isinstance(cls.permissions, list):\n            cls.permissions = [cls.permissions]\n        # Creating API instance to validate the attributes.\n        API(\n            input_model=cls.input_model,\n            output_model=cls.output_model,\n            output_schema=cls.output_schema,\n            auth=cls.auth,\n            permissions=cls.permissions,\n            throttling=cls.throttling,\n            cache=cls.cache,\n            middlewares=cls.middlewares,\n        )\n\n    async def get(self, *args, **kwargs):\n        raise MethodNotAllowedAPIError\n\n    async def post(self, *args, **kwargs):\n        raise MethodNotAllowedAPIError\n\n    async def put(self, *args, **kwargs):\n        raise MethodNotAllowedAPIError\n\n    async def patch(self, *args, **kwargs):\n        raise MethodNotAllowedAPIError\n\n    async def delete(self, *args, **kwargs):\n        raise MethodNotAllowedAPIError\n\n    async def call_method(self, request: Request):\n        match request.method:\n            case 'GET':\n                func = self.get\n            case 'POST':\n                func = self.post\n            case 'PUT':\n                func = self.put\n            case 'PATCH':\n                func = self.patch\n            case 'DELETE':\n                func = self.delete\n            case _:\n                raise MethodNotAllowedAPIError\n\n        return await API(\n            input_model=self.input_model,\n            output_model=self.output_model,\n            output_schema=self.output_schema,\n            auth=self.auth,\n            permissions=self.permissions,\n            throttling=self.throttling,\n            cache=self.cache,\n            middlewares=self.middlewares,\n        )(func)(request=request)\n"
  },
  {
    "path": "panther/authentications.py",
    "content": "import logging\nimport time\nfrom abc import abstractmethod\nfrom datetime import datetime, timezone\nfrom typing import Literal\n\nfrom panther.base_websocket import Websocket\nfrom panther.configs import config\nfrom panther.db.connections import redis\nfrom panther.db.models import Model\nfrom panther.exceptions import AuthenticationAPIError\nfrom panther.request import Request\nfrom panther.utils import generate_hash_value_from_string\n\ntry:\n    from jose import JWTError, jwt\nexcept ImportError as e:\n    # This `JWTError` and `jwt` is not going to be used,\n    #   If user really wants to use redis,\n    #   we are going to force him to install it in `panther._load_configs.load_jwt_config`\n    JWTError = type('JWTError', (), {})\n    jwt = type('jwt', (), {})\n\n\nlogger = logging.getLogger('panther')\n\n\nclass BaseAuthentication:\n    @abstractmethod\n    async def __call__(self, request: Request | Websocket):\n        \"\"\"Return Instance of User\"\"\"\n        msg = f'{self.__class__.__name__}.__call__() is not implemented.'\n        raise self.exception(msg) from None\n\n    @classmethod\n    def exception(cls, message: str | Exception, /) -> type[AuthenticationAPIError]:\n        logger.error(f'{cls.__name__} Error: \"{message}\"')\n        return AuthenticationAPIError\n\n\nclass JWTAuthentication(BaseAuthentication):\n    \"\"\"\n    Retrieve the Authorization from header\n    Example:\n        Headers: {'authorization': 'Bearer the_jwt_token'}\n    \"\"\"\n\n    model = None\n    keyword = 'Bearer'\n    algorithm = 'HS256'\n    HTTP_HEADER_ENCODING = 'iso-8859-1'  # RFC5987\n\n    async def __call__(self, request: Request | Websocket) -> Model | None:\n        \"\"\"Authenticate the user based on the JWT token in the Authorization header.\"\"\"\n        auth_header = self.get_authorization_header(request)\n        # Set None as `request.user`\n        if auth_header is None:\n            return None\n\n        token = self.get_token(auth_header=auth_header)\n        if redis.is_connected and await self.is_token_revoked(token=token):\n            msg = 'User logged out'\n            raise self.exception(msg) from None\n        payload = await self.decode_jwt(token)\n        user = await self.get_user(payload)\n        user._auth_token = token\n        return user\n\n    @classmethod\n    def get_authorization_header(cls, request: Request | Websocket) -> list[str] | None:\n        \"\"\"Retrieve the Authorization header from the request.\"\"\"\n        if auth := request.headers.authorization:\n            return auth.split()\n        return None\n\n    @classmethod\n    def get_token(cls, auth_header):\n        if len(auth_header) != 2:\n            msg = 'Authorization header must contain 2 parts'\n            raise cls.exception(msg) from None\n\n        bearer, token = auth_header\n\n        try:\n            token.encode(JWTAuthentication.HTTP_HEADER_ENCODING)\n        except UnicodeEncodeError as e:\n            raise cls.exception(e) from None\n\n        if bearer.lower() != cls.keyword.lower():\n            msg = 'Authorization keyword is not valid'\n            raise cls.exception(msg) from None\n\n        return token\n\n    @classmethod\n    async def decode_jwt(cls, token: str) -> dict:\n        \"\"\"Decode a JWT token and return the payload.\"\"\"\n        try:\n            return jwt.decode(\n                token=token,\n                key=config.JWT_CONFIG.key,\n                algorithms=[config.JWT_CONFIG.algorithm],\n            )\n        except JWTError as e:\n            raise cls.exception(e) from None\n\n    @classmethod\n    async def get_user(cls, payload: dict) -> Model:\n        \"\"\"Fetch the user based on the decoded JWT payload from cls.model or config.UserModel\"\"\"\n        if (user_id := payload.get('user_id')) is None:\n            msg = 'Payload does not have `user_id`'\n            raise cls.exception(msg)\n\n        user_model = cls.model or config.USER_MODEL\n        user = await user_model.find_one(id=user_id)\n        if user is None:\n            raise cls.exception('User not found')\n\n        return user\n\n    @classmethod\n    def encode_jwt(cls, user_id: str, token_type: Literal['access', 'refresh'] = 'access') -> str:\n        \"\"\"Generate a JWT token for a given user ID.\"\"\"\n        issued_at = datetime.now(timezone.utc).timestamp()\n        if token_type == 'access':\n            expire = issued_at + config.JWT_CONFIG.life_time\n        else:\n            expire = issued_at + config.JWT_CONFIG.refresh_life_time\n\n        claims = {\n            'token_type': token_type,\n            'user_id': user_id,\n            'iat': issued_at,\n            'exp': expire,\n        }\n        return jwt.encode(\n            claims,\n            key=config.JWT_CONFIG.key,\n            algorithm=config.JWT_CONFIG.algorithm,\n        )\n\n    @classmethod\n    async def login(cls, user) -> dict:\n        \"\"\"Generate access and refresh tokens for user login.\"\"\"\n        return {\n            'access_token': cls.encode_jwt(user_id=user.id),\n            'refresh_token': cls.encode_jwt(user_id=user.id, token_type='refresh'),\n        }\n\n    @classmethod\n    async def logout(cls, user) -> None:\n        \"\"\"Log out a user by revoking their JWT token.\"\"\"\n        payload = await cls.decode_jwt(token=user._auth_token)\n        await cls.revoke_token_in_cache(token=user._auth_token, exp=payload['exp'])\n\n    @classmethod\n    async def refresh(cls, user):\n        if hasattr(user, '_auth_refresh_token'):\n            # It happens in CookieJWTAuthentication\n            token = user._auth_refresh_token\n        else:\n            token = user._auth_token\n\n        payload = await cls.decode_jwt(token=token)\n\n        if payload['token_type'] != 'refresh':\n            raise cls.exception('Invalid token type; expected `refresh` token.')\n        # Revoke after use\n        await cls.revoke_token_in_cache(token=token, exp=payload['exp'])\n\n        return await cls.login(user=user)\n\n    @classmethod\n    async def revoke_token_in_cache(cls, token: str, exp: int) -> None:\n        \"\"\"Mark the token as revoked in the cache.\"\"\"\n        if redis.is_connected:\n            key = generate_hash_value_from_string(token)\n            remaining_exp_time = int(exp - time.time())\n            await redis.set(key, b'', ex=remaining_exp_time)\n        else:\n            logger.error('Redis is not connected; token revocation is not effective.')\n\n    @classmethod\n    async def is_token_revoked(cls, token: str) -> bool:\n        \"\"\"Check if the token is revoked by looking it up in the cache.\"\"\"\n        key = generate_hash_value_from_string(token)\n        return bool(await redis.exists(key))\n\n\nclass QueryParamJWTAuthentication(JWTAuthentication):\n    \"\"\"\n    Retrieve the Authorization from query params\n    Example:\n        https://example.com?authorization=the_jwt_without_bearer\n    \"\"\"\n\n    @classmethod\n    def get_authorization_header(cls, request: Request | Websocket) -> str | None:\n        if 'authorization' in request.query_params:\n            return request.query_params['authorization']\n        return None\n\n    @classmethod\n    def get_token(cls, auth_header) -> str:\n        return auth_header\n\n\nclass CookieJWTAuthentication(JWTAuthentication):\n    \"\"\"\n    Retrieve the Authorization from cookies\n    Example:\n        Cookies: access_token=the_jwt_without_bearer\n    \"\"\"\n\n    async def __call__(self, request: Request | Websocket) -> Model:\n        user = await super().__call__(request=request)\n        if refresh_token := request.headers.get_cookies().get('refresh_token'):\n            # It's used in `cls.refresh()`\n            user._auth_refresh_token = refresh_token\n        return user\n\n    @classmethod\n    def get_authorization_header(cls, request: Request | Websocket) -> str | None:\n        if token := request.headers.get_cookies().get('access_token'):\n            return token\n        return None\n\n    @classmethod\n    def get_token(cls, auth_header) -> str:\n        return auth_header\n"
  },
  {
    "path": "panther/background_tasks.py",
    "content": "\"\"\"\nExample:\n-------------------------------------------------------------\n>>> import datetime\n\n\n>>> async def hello(name: str):\n>>>     print(f'Hello {name}')\n\n# Run it every 5 seconds for 2 times\n>>> BackgroundTask(hello, 'Ali').interval(2).every_seconds(5).submit()\n\n# Run it every day at 08:00 O'clock forever\n>>> BackgroundTask(hello, 'Saba').interval(-1).every_days().at(datetime.time(hour=8)).submit()\n\"\"\"\n\nimport asyncio\nimport datetime\nimport logging\nimport sys\nimport time\nfrom enum import Enum\nfrom threading import Lock, Thread\nfrom typing import TYPE_CHECKING, Any, Literal\n\nfrom panther._utils import is_function_async\nfrom panther.utils import Singleton, timezone_now\n\nif TYPE_CHECKING:\n    from collections.abc import Callable\n\n__all__ = ('BackgroundTask', 'WeekDay')\n\nlogger = logging.getLogger('panther')\n_application_event_loop: asyncio.AbstractEventLoop | None = None\n\n\ndef register_application_event_loop(loop: asyncio.AbstractEventLoop | None) -> None:\n    global _application_event_loop\n    _application_event_loop = loop\n\n\ndef get_application_event_loop() -> asyncio.AbstractEventLoop | None:\n    return _application_event_loop\n\n\nif sys.version_info.minor >= 11:\n    from typing import Self\nelse:\n    from typing import TypeVar\n\n    Self = TypeVar('Self', bound='BackgroundTask')\n\n\nclass WeekDay(Enum):\n    MONDAY = 0\n    TUESDAY = 1\n    WEDNESDAY = 2\n    THURSDAY = 3\n    FRIDAY = 4\n    SATURDAY = 5\n    SUNDAY = 6\n\n\nclass BackgroundTask:\n    \"\"\"\n    Schedules and runs a function periodically in the background.\n\n    Default: Task runs once. If only a custom interval is specified, default interval time is 1 minute.\n    Use submit() to add the task to the background queue.\n    \"\"\"\n\n    def __init__(self, func: 'Callable', *args: Any, **kwargs: Any):\n        self._func: 'Callable' = func\n        self._args: tuple = args\n        self._kwargs: dict = kwargs\n        self._remaining_interval: int = 1\n        self._last_run: datetime.datetime | None = None\n        self._timedelta: datetime.timedelta = datetime.timedelta(minutes=1)\n        self._time: datetime.time | None = None\n        self._day_of_week: WeekDay | None = None\n        self._unit: Literal['seconds', 'minutes', 'hours', 'days', 'weeks'] | None = None\n\n    def interval(self, interval: int, /) -> Self:\n        \"\"\"Set how many times to run the task. interval = -1 for infinite.\"\"\"\n        self._remaining_interval = interval\n        return self\n\n    def every_seconds(self, seconds: int = 1, /) -> Self:\n        \"\"\"Run every N seconds (default 1).\"\"\"\n        self._unit = 'seconds'\n        self._timedelta = datetime.timedelta(seconds=seconds)\n        return self\n\n    def every_minutes(self, minutes: int = 1, /) -> Self:\n        \"\"\"Run every N minutes (default 1).\"\"\"\n        self._unit = 'minutes'\n        self._timedelta = datetime.timedelta(minutes=minutes)\n        return self\n\n    def every_hours(self, hours: int = 1, /) -> Self:\n        \"\"\"Run every N hours (default 1).\"\"\"\n        self._unit = 'hours'\n        self._timedelta = datetime.timedelta(hours=hours)\n        return self\n\n    def every_days(self, days: int = 1, /) -> Self:\n        \"\"\"Run every N days (default 1).\"\"\"\n        self._unit = 'days'\n        self._timedelta = datetime.timedelta(days=days)\n        return self\n\n    def every_weeks(self, weeks: int = 1, /) -> Self:\n        \"\"\"Run every N weeks (default 1).\"\"\"\n        self._unit = 'weeks'\n        self._timedelta = datetime.timedelta(weeks=weeks)\n        return self\n\n    def on(self, day_of_week: WeekDay, /) -> Self:\n        \"\"\"\n        Set day to schedule the task. Accepts string like 'monday', 'tuesday', etc.\n        \"\"\"\n        self._day_of_week = day_of_week\n        return self\n\n    def at(self, _time: datetime.time, /) -> Self:\n        \"\"\"Set a time to schedule the task.\"\"\"\n        if isinstance(_time, datetime.time):\n            self._time = _time\n        elif isinstance(_time, datetime.datetime):\n            self._time = _time.time()\n        else:\n            raise TypeError(\n                f'Argument should be instance of `datetime.time()` or `datetime.datetime()` not `{type(_time)}`',\n            )\n        return self\n\n    def _should_wait(self) -> bool:\n        \"\"\"\n        Returns True if the task should wait (not run yet), False if it should run now.\n        \"\"\"\n        now = timezone_now()\n\n        # Wait\n        if self._last_run and (self._last_run + self._timedelta) > now:\n            return True\n\n        # Check day of week\n        if self._day_of_week is not None and self._day_of_week.value != now.weekday():\n            return True\n\n        # We don't have time condition, so run\n        if self._time is None:\n            self._last_run = now\n            return False\n\n        # Time is ok, so run\n        if now.hour == self._time.hour and now.minute == self._time.minute and now.second == self._time.second:\n            self._last_run = now\n            return False\n\n        # Time was not ok, wait\n        return True\n\n    def __call__(self) -> bool:\n        \"\"\"\n        Executes the task if it's time. Returns True if the task should remain scheduled, False if done.\n        \"\"\"\n        if self._remaining_interval == 0:\n            return False\n        if self._should_wait():\n            return True\n        logger.info(\n            f'{self._func.__name__}('\n            f'{\", \".join(str(a) for a in self._args)}, '\n            f'{\", \".join(f\"{k}={v}\" for k, v in self._kwargs.items())}'\n            f') Remaining Interval -> {\"∞\" if self._remaining_interval == -1 else self._remaining_interval - 1}',\n        )\n        if self._remaining_interval != -1:\n            self._remaining_interval -= 1\n        try:\n            if is_function_async(self._func):\n                coroutine = self._func(*self._args, **self._kwargs)\n                app_loop = _application_event_loop\n                if app_loop and app_loop.is_running() and not app_loop.is_closed():\n                    asyncio.run_coroutine_threadsafe(coroutine, app_loop).result()\n                else:\n                    asyncio.run(coroutine)\n            else:\n                self._func(*self._args, **self._kwargs)\n        except Exception as e:\n            logger.error(f'Exception in background task {self._func.__name__}: {e}', exc_info=True)\n        return True\n\n    def submit(self) -> Self:\n        \"\"\"Add this task to the background task queue.\"\"\"\n        _background_tasks.add_task(self)\n        return self\n\n\nclass BackgroundTasks(Singleton):\n    _initialized: bool = False\n\n    def __init__(self):\n        self.tasks: list[BackgroundTask] = []\n        self._lock = Lock()\n\n    def add_task(self, task: BackgroundTask):\n        if self._initialized is False:\n            logger.error('Task will be ignored, `BACKGROUND_TASKS` is not True in `configs`')\n            return\n        if not self._is_instance_of_task(task):\n            return\n        with self._lock:\n            if task not in self.tasks:\n                self.tasks.append(task)\n                logger.info(f'Task {task._func.__name__} submitted.')\n\n    def initialize(self):\n        \"\"\"Call once to start background task processing.\"\"\"\n        if self._initialized is False:\n            self._initialized = True\n            Thread(target=self._run_tasks, daemon=True).start()\n\n    def _run_task(self, task: BackgroundTask):\n        should_continue = task()\n        if should_continue is False:\n            with self._lock:\n                if task in self.tasks:\n                    self.tasks.remove(task)\n\n    def _run_tasks(self):\n        while True:\n            with self._lock:\n                tasks_snapshot = self.tasks[:]\n            for task in tasks_snapshot:\n                Thread(target=self._run_task, args=(task,)).start()\n            time.sleep(1)\n\n    @classmethod\n    def _is_instance_of_task(cls, task: Any, /) -> bool:\n        if not isinstance(task, BackgroundTask):\n            name = getattr(task, '__name__', task.__class__.__name__)\n            logger.error(f'`{name}` should be instance of `background_tasks.BackgroundTask`')\n            return False\n        return True\n\n\n_background_tasks = BackgroundTasks()\n"
  },
  {
    "path": "panther/base_request.py",
    "content": "from collections.abc import Callable\nfrom typing import TYPE_CHECKING\nfrom urllib.parse import parse_qsl\n\nfrom panther.exceptions import InvalidPathVariableAPIError\n\nif TYPE_CHECKING:\n    from panther.db import Model\n\n\nclass Headers:\n    accept: str\n    accept_encoding: str\n    accept_language: str\n    authorization: str\n    cache_control: str\n    connection: str\n    content_length: str\n    content_type: str\n    host: str\n    origin: str\n    pragma: str\n    referer: str\n    sec_fetch_dest: str\n    sec_fetch_mode: str\n    sec_fetch_site: str\n    user_agent: str\n\n    upgrade: str\n    sec_websocket_version: str\n    sec_websocket_key: str\n\n    def __init__(self, headers: list):\n        self.__headers = {header[0].decode('utf-8'): header[1].decode('utf-8') for header in headers}\n        self.__pythonic_headers = {k.lower().replace('-', '_'): v for k, v in self.__headers.items()}\n\n    def __getattr__(self, item: str):\n        if result := self.__pythonic_headers.get(item):\n            return result\n        return self.__headers.get(item)\n\n    def __getitem__(self, item: str):\n        if result := self.__headers.get(item):\n            return result\n        return self.__pythonic_headers.get(item)\n\n    def __str__(self):\n        items = ', '.join(f'{k}={v}' for k, v in self.__headers.items())\n        return f'Headers({items})'\n\n    def __contains__(self, item):\n        return (item in self.__headers) or (item in self.__pythonic_headers)\n\n    __repr__ = __str__\n\n    @property\n    def __dict__(self):\n        return self.__headers\n\n    def get_cookies(self) -> dict:\n        \"\"\"\n        Example of `request.headers.cookie`:\n            'csrftoken=aaa; sessionid=bbb; access_token=ccc; refresh_token=ddd'\n\n        Example of `request.headers.get_cookies()`:\n            {\n                'csrftoken': 'aaa',\n                'sessionid': 'bbb',\n                'access_token': 'ccc',\n                'refresh_token': 'ddd',\n            }\n        \"\"\"\n        if self.cookie:\n            return {k.strip(): v for k, v in (c.split('=', maxsplit=1) for c in self.cookie.split(';'))}\n        return {}\n\n\nclass Address:\n    def __init__(self, ip, port):\n        self.ip = ip\n        self.port = port\n\n    def __str__(self):\n        return f'{self.ip}:{self.port}'\n\n\nclass BaseRequest:\n    def __init__(self, scope: dict, receive: Callable, send: Callable):\n        self.scope = scope\n        self.asgi_send = send\n        self.asgi_receive = receive\n        self._headers: Headers | None = None\n        self._params: dict | None = None\n        self.user: Model | None = None\n        self.path_variables: dict | None = None\n\n    @property\n    def headers(self) -> Headers:\n        if self._headers is None:\n            self._headers = Headers(self.scope['headers'])\n        return self._headers\n\n    @property\n    def query_params(self) -> dict:\n        if self._params is None:\n            self._params = {k: v for k, v in parse_qsl(self.scope['query_string'].decode('utf-8'))}\n        return self._params\n\n    @property\n    def path(self) -> str:\n        return self.scope['path']\n\n    @property\n    def server(self) -> Address:\n        return Address(*self.scope['server'])\n\n    @property\n    def client(self) -> Address:\n        return Address(*self.scope['client'])\n\n    @property\n    def http_version(self) -> str:\n        return self.scope['http_version']\n\n    @property\n    def scheme(self) -> str:\n        return self.scope['scheme']\n\n    def collect_path_variables(self, found_path: str):\n        self.path_variables = {\n            variable.strip('< >'): value\n            for variable, value in zip(found_path.strip('/').split('/'), self.path.strip('/').split('/'))\n            if variable.startswith('<')\n        }\n\n    def clean_parameters(self, function_annotations: dict) -> dict:\n        kwargs = self.path_variables.copy()\n\n        for variable_name, variable_type in function_annotations.items():\n            # Put Request/ Websocket In kwargs (If User Wants It)\n            if issubclass(variable_type, BaseRequest):\n                kwargs[variable_name] = self\n\n            elif variable_name in kwargs:\n                # Cast To Boolean\n                if variable_type is bool:\n                    value = kwargs[variable_name].lower()\n                    if value in ['false', '0']:\n                        kwargs[variable_name] = False\n                    elif value in ['true', '1']:\n                        kwargs[variable_name] = True\n                    else:\n                        raise InvalidPathVariableAPIError(value=kwargs[variable_name], variable_type=variable_type)\n\n                # Cast To Int\n                elif variable_type is int:\n                    try:\n                        kwargs[variable_name] = int(kwargs[variable_name])\n                    except ValueError:\n                        raise InvalidPathVariableAPIError(value=kwargs[variable_name], variable_type=variable_type)\n        return kwargs\n"
  },
  {
    "path": "panther/base_websocket.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport inspect\nimport logging\nfrom multiprocessing.managers import SyncManager\nfrom typing import TYPE_CHECKING, Any, Callable, Literal\n\nimport orjson as json\nimport ulid\n\nfrom panther import status\nfrom panther.base_request import BaseRequest\nfrom panther.configs import config\nfrom panther.db.connections import redis\nfrom panther.exceptions import BaseError, InvalidPathVariableAPIError\nfrom panther.utils import Singleton\n\nif TYPE_CHECKING:\n    from redis.asyncio import Redis\n\nlogger = logging.getLogger('panther')\n\n\nclass PubSub:\n    def __init__(self, manager: SyncManager):\n        self._manager = manager\n        self._subscribers = self._manager.list()\n\n    def subscribe(self):\n        queue = self._manager.Queue()\n        self._subscribers.append(queue)\n        return queue\n\n    def publish(self, msg):\n        for queue in self._subscribers:\n            queue.put(msg)\n\n\nclass WebsocketConnections(Singleton):\n    def __init__(self, pubsub_connection: Redis | SyncManager):\n        self.connections = {}\n        self.connections_count = 0\n        self.pubsub_connection = pubsub_connection\n\n        if isinstance(self.pubsub_connection, SyncManager):\n            self.pubsub = PubSub(manager=self.pubsub_connection)\n\n    async def __call__(self):\n        if isinstance(self.pubsub_connection, SyncManager):\n            # We don't have redis connection, so use the `multiprocessing.Manager`\n            self.pubsub: PubSub\n            queue = self.pubsub.subscribe()\n            logger.info(\"Subscribed to 'websocket_connections' queue\")\n            while True:\n                try:\n                    received_message = await asyncio.to_thread(queue.get)\n                    if received_message is None:\n                        # The None came from the CancelledError, so break the loop\n                        break\n                    await self._handle_received_message(received_message=received_message)\n                except (InterruptedError, asyncio.CancelledError):\n                    # Put the None to the queue, so the executor knows that it ends\n                    queue.put(None)\n                    break\n        else:\n            # We have a redis connection, so use it for pubsub\n            self.pubsub = self.pubsub_connection.pubsub()\n            await self.pubsub.subscribe('websocket_connections')\n            logger.info(\"Subscribed to 'websocket_connections' channel\")\n            async for channel_data in self.pubsub.listen():\n                match channel_data['type']:\n                    # Subscribed\n                    case 'subscribe':\n                        continue\n\n                    # Message Received\n                    case 'message':\n                        loaded_data = json.loads(channel_data['data'].decode())\n                        await self._handle_received_message(received_message=loaded_data)\n\n                    case unknown_type:\n                        logger.error(f'Unknown Channel Type: {unknown_type}')\n\n    async def _handle_received_message(self, received_message):\n        if (\n            isinstance(received_message, dict)\n            and (connection_id := received_message.get('connection_id'))\n            and connection_id in self.connections\n            and 'action' in received_message\n            and 'data' in received_message\n        ):\n            # Check Action of WS\n            match received_message['action']:\n                case 'send':\n                    await self.connections[connection_id].send(data=received_message['data'])\n                case 'close':\n                    await self.connections[connection_id].close(\n                        code=received_message['data']['code'],\n                        reason=received_message['data']['reason'],\n                    )\n                case unknown_action:\n                    logger.error(f'Unknown Message Action: {unknown_action}')\n\n    async def publish(self, connection_id: str, action: Literal['send', 'close'], data: any):\n        publish_data = {'connection_id': connection_id, 'action': action, 'data': data}\n\n        if redis.is_connected:\n            await redis.publish('websocket_connections', json.dumps(publish_data))\n        else:\n            self.pubsub.publish(publish_data)\n\n    async def listen(self, connection: Websocket):\n        # 1. Authentication\n        if not connection.is_rejected:\n            await self.handle_authentication(connection=connection)\n\n        # 2. Permissions\n        if not connection.is_rejected:\n            await self.handle_permissions(connection=connection)\n\n        if connection.is_rejected:\n            # Connection is rejected so don't continue the flow ...\n            return None\n\n        # 3. Put PathVariables and Request(If User Wants It) In kwargs\n        try:\n            kwargs = connection.clean_parameters(connection.connect.__annotations__)\n        except InvalidPathVariableAPIError as e:\n            connection.change_state(state='Rejected', message=e.detail)\n            return await connection.close()\n\n        # 4. Connect To Endpoint\n        await connection.connect(**kwargs)\n\n        # 5. Check Connection\n        if not connection.is_connected and not connection.is_rejected:\n            # User didn't call the `self.accept()` or `self.close()` so we `close()` the connection (reject)\n            return await connection.close()\n\n        # 6. Listen Connection\n        await self.listen_connection(connection=connection)\n\n        return connection\n\n    async def listen_connection(self, connection: Websocket):\n        while True:\n            response = await connection.asgi_receive()\n            if response['type'] == 'websocket.connect':\n                continue\n\n            if response['type'] == 'websocket.disconnect':\n                # Connection has to be closed by the client.\n                await self.connection_closed(connection=connection)\n                break\n\n            if 'text' in response:\n                await connection.receive(data=response['text'])\n            else:\n                await connection.receive(data=response['bytes'])\n\n    async def connection_accepted(self, connection: Websocket) -> None:\n        # Generate ConnectionID\n        connection._connection_id = ulid.new()\n\n        # Save Connection\n        self.connections[connection.connection_id] = connection\n        connection.change_state(state='Accepted')\n\n    async def connection_closed(self, connection: Websocket, from_server: bool = False) -> None:\n        if connection.is_connected:\n            del self.connections[connection.connection_id]\n            connection.change_state(state='Closed')\n            connection._connection_id = ''\n\n        elif connection.is_rejected is False and from_server is True:\n            connection.change_state(state='Rejected')\n            connection._is_rejected = True\n\n    async def start(self):\n        \"\"\"\n        Start Websocket Listener (Redis/ Queue)\n\n        Cause of --preload in gunicorn we have to keep this function here,\n        and we can't move it to __init__ of Panther\n\n            * Each process should start this listener for itself,\n              but they have same Manager()\n        \"\"\"\n\n        # Schedule the async function to run in the background,\n        #   We don't need to await for this task\n        asyncio.create_task(self())\n\n    @classmethod\n    async def handle_authentication(cls, connection: Websocket):\n        if auth := (connection.auth or config.WS_AUTHENTICATION):\n            if inspect.isclass(auth):\n                auth = auth()\n            try:\n                connection.user = await auth(connection)\n            except BaseError as e:\n                connection.change_state(state='Rejected', message=e.detail)\n                await connection.close()\n\n    @classmethod\n    async def handle_permissions(cls, connection: Websocket):\n        permissions = connection.permissions\n        if permissions is not None and not isinstance(permissions, list):\n            permissions = [permissions]\n\n        if permissions:\n            for perm in permissions:\n                if inspect.isclass(perm):\n                    perm = perm()\n                if await perm(connection) is False:\n                    connection.change_state(state='Rejected', message='Permission Denied')\n                    await connection.close()\n\n\nclass Websocket(BaseRequest):\n    auth: Callable | None = None\n    permissions: list[Callable] | Callable | None = None\n    state: str = 'Connected'\n    _connection_id: str = ''\n    _is_rejected: bool = False\n\n    def __init_subclass__(cls, **kwargs):\n        if cls.__module__ != 'panther.websocket':\n            config.HAS_WS = True\n\n    async def connect(self, **kwargs) -> None:\n        pass\n\n    async def receive(self, data: str | bytes) -> None:\n        pass\n\n    async def accept(self, subprotocol: str | None = None, headers: dict | None = None) -> None:\n        await self.asgi_send({'type': 'websocket.accept', 'subprotocol': subprotocol, 'headers': headers or {}})\n        await config.WEBSOCKET_CONNECTIONS.connection_accepted(connection=self)\n\n    async def send(self, data: any = None) -> None:\n        logger.debug(f'Sending WS Message to {self.connection_id}')\n        if isinstance(data, bytes):\n            await self.send_bytes(bytes_data=data)\n        elif isinstance(data, str):\n            await self.send_text(text_data=data)\n        else:\n            await self.send_text(text_data=json.dumps(data).decode())\n\n    async def send_text(self, text_data: str) -> None:\n        await self.asgi_send({'type': 'websocket.send', 'text': text_data})\n\n    async def send_bytes(self, bytes_data: bytes) -> None:\n        await self.asgi_send({'type': 'websocket.send', 'bytes': bytes_data})\n\n    async def close(self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = '') -> None:\n        await self.asgi_send({'type': 'websocket.close', 'code': code, 'reason': reason})\n        await config.WEBSOCKET_CONNECTIONS.connection_closed(connection=self, from_server=True)\n\n    @property\n    def connection_id(self) -> str:\n        if self.is_connected:\n            return self._connection_id\n        logger.error('You should first `self.accept()` the connection then use the `self.connection_id`')\n\n    @property\n    def is_connected(self) -> bool:\n        return bool(self._connection_id)\n\n    @property\n    def is_rejected(self) -> bool:\n        return self._is_rejected\n\n    def change_state(self, state: Literal['Accepted', 'Closed', 'Rejected'], message: str = ''):\n        self.state = state\n        if message:\n            message = f' | {message}'\n        if self.is_connected:\n            message = f' | {self.connection_id}{message}'\n        logger.debug(f'WS {self.path} --> {state}{message}')\n"
  },
  {
    "path": "panther/caching.py",
    "content": "import logging\nfrom collections import namedtuple\nfrom datetime import datetime, timedelta\n\nimport orjson as json\n\nfrom panther.db.connections import redis\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.utils import generate_hash_value_from_string, round_datetime\n\nlogger = logging.getLogger('panther')\n\ncaches: dict[str, tuple[bytes, dict, int]] = {}\nCachedResponse = namedtuple('CachedResponse', ['data', 'headers', 'status_code'])\n\n\ndef api_cache_key(request: Request, duration: timedelta | None = None) -> str:\n    client = (request.user and request.user.id) or request.client.ip\n    query_params_hash = generate_hash_value_from_string(request.scope['query_string'].decode('utf-8'))\n    key = f'{client}-{request.path}-{query_params_hash}-{request.validated_data}'\n\n    if duration:\n        time = round_datetime(datetime.now(), duration)\n        return f'{time}-{key}'\n\n    return key\n\n\nasync def get_response_from_cache(*, request: Request, duration: timedelta) -> CachedResponse | None:\n    \"\"\"\n    If redis.is_connected:\n        Get Cached Data From Redis\n    else:\n        Get Cached Data From Memory\n    \"\"\"\n    if redis.is_connected:\n        key = api_cache_key(request=request)\n        data = (await redis.get(key) or b'{}').decode()\n        if value := json.loads(data):\n            return CachedResponse(data=value[0].encode(), headers=value[1], status_code=value[2])\n    else:\n        key = api_cache_key(request=request, duration=duration)\n        if value := caches.get(key):\n            return CachedResponse(*value)\n\n\nasync def set_response_in_cache(*, request: Request, response: Response, duration: timedelta | int) -> None:\n    \"\"\"\n    If redis.is_connected:\n        Cache The Data In Redis\n    else:\n        Cache The Data In Memory\n    \"\"\"\n\n    if redis.is_connected:\n        key = api_cache_key(request=request)\n        cache_data: tuple[str, dict, int] = (response.body.decode(), response.headers, response.status_code)\n        await redis.set(key, json.dumps(cache_data), ex=duration)\n\n    else:\n        key = api_cache_key(request=request, duration=duration)\n        caches[key] = (response.body, response.headers, response.status_code)\n        logger.info('`cache` is not very accurate when `redis` is not connected.')\n"
  },
  {
    "path": "panther/cli/__init__.py",
    "content": ""
  },
  {
    "path": "panther/cli/create_project_command.py",
    "content": "from collections.abc import Callable\nfrom pathlib import Path\n\nfrom rich import print as rich_print\nfrom rich.console import Console\nfrom rich.progress import ProgressBar\nfrom rich.prompt import Prompt\n\nfrom panther import version\nfrom panther.cli.template import (\n    AUTHENTICATION_PART,\n    BASE_DIR_PART,\n    DATABASE_MONGODB_PART,\n    DATABASE_PANTHERDB_PART,\n    IMPORT_COMPLETE_LOAD_ENV_PART,\n    IMPORT_LOAD_ENV_PART,\n    IMPORT_PATH_PART,\n    REDIS_PART,\n    SECRET_KEY_PART,\n    SINGLE_FILE_TEMPLATE,\n    TEMPLATE,\n    USER_MODEL_PART,\n)\nfrom panther.cli.utils import cli_error\n\n\nclass CreateProject:\n    # ERASE_LINE = 100 * ' '\n    ERASE_LINE = '\\x1b[2K'\n    # REMOVE_LAST_LINE = f'\\033[1A[{ERASE_LINE}'\n    REMOVE_LAST_LINE = f'\\x1b[1A{ERASE_LINE}'\n\n    def __init__(self):\n        self.console = Console()\n        self.input_console = Console(style='bold magenta')\n        self.project_name = ''\n        self.base_directory = '.'\n        self.database = '0'\n        self.database_encryption = False\n        self.redis = False\n        self.authentication = False\n        self.monitoring = True\n        self.log_queries = True\n        self.single_file = False\n        self.questions = [\n            {\n                'field': 'project_name',\n                'message': 'Project Name',\n                'validation_func': lambda x: x != '',\n                'error_message': \"'{}' is not valid, Can't be empty.\",\n            },\n            {\n                'field': 'base_directory',\n                'message': 'Directory (default is ./)',\n                'validation_func': self._check_all_directories,\n                'error_message': '\"{}\" directory already exists.',\n                'show_validation_error': True,\n            },\n            {\n                'field': 'single_file',\n                'message': 'Do you want Single-File project',\n                'is_boolean': True,\n            },\n            {\n                'field': 'database',\n                'message': '    0: PantherDB (File-Base, No Requirements)\\n    1: MongoDB (Required `motor`)\\n    2: No Database\\nChoose your database (default is 0)',\n                'validation_func': lambda x: x in ['0', '1', '2'],\n                'error_message': \"Invalid choice, '{}' not in ['0', '1', '2']\",\n            },\n            {\n                'field': 'redis',\n                'message': 'Do you want to use Redis (Required `redis`)',\n                'is_boolean': True,\n            },\n            {\n                'field': 'authentication',\n                'message': 'Do you want to use JWT Authentication (Required `python-jose`)',\n                'is_boolean': True,\n            },\n        ]\n        self.progress_len = len(self.questions)\n        self.bar = ProgressBar(total=self.progress_len, width=40)\n\n    def create(self, args: list) -> None:\n        # Get Project Name\n        if len(args) == 0:\n            try:\n                self.collect_creation_data()\n            except KeyboardInterrupt:\n                return self.console.print('\\nKeyboardInterrupt', style='bold red')\n        else:\n            self.project_name = args[0]\n            # Get Base Directory\n            self.base_directory: str = '.'\n            if len(args) > 1:\n                self.base_directory = args[1]\n\n            existence = self._check_all_directories(self.base_directory, return_error=True)\n            if existence is not True:\n                return cli_error(f'\"{existence}\" directory already exists.')\n\n        self.project_name = self.project_name.lower().replace(' ', '_')\n        self.base_directory = self.base_directory.replace(' ', '_')\n        template = SINGLE_FILE_TEMPLATE if self.single_file else TEMPLATE\n\n        # Create Base Directory\n        if self.base_directory != '.':\n            Path(self.base_directory).mkdir()\n\n        for file_name, data in template.items():\n            path = f'{self.base_directory}/{file_name}'\n            if isinstance(data, str):\n                # Create File\n                self._create_file(path=path, data=data)\n            else:\n                # Create Sub Directory\n                Path(path).mkdir()\n\n                # Create Files of Sub Directory\n                for sub_file_name, sub_data in data.items():\n                    inner_path = f'{path}/{sub_file_name}'\n                    self._create_file(path=inner_path, data=sub_data)\n\n    def _create_file(self, *, path: str, data: str):\n        base_dir_part = BASE_DIR_PART if self.authentication or self.database == '1' else ''\n        import_load_env_part = IMPORT_LOAD_ENV_PART if self.authentication else ''\n        import_complete_load_env_part = IMPORT_COMPLETE_LOAD_ENV_PART if self.authentication else ''\n        import_path_part = IMPORT_PATH_PART if self.authentication or self.database == '1' else ''\n        user_model_part = USER_MODEL_PART if self.authentication else ''\n        authentication_part = AUTHENTICATION_PART if self.authentication else ''\n        secret_key_part = SECRET_KEY_PART if self.authentication else ''\n        redis_part = REDIS_PART if self.redis else ''\n        if self.database == '0':\n            database_part = DATABASE_PANTHERDB_PART\n        elif self.database == '1':\n            database_part = DATABASE_MONGODB_PART\n        else:\n            database_part = ''\n\n        data = data.replace('{IMPORT_LOAD_ENV}', import_load_env_part)\n        data = data.replace('{IMPORT_COMPLETE_LOAD_ENV}', import_complete_load_env_part)\n        data = data.replace('{IMPORT_PATH}', import_path_part)\n        data = data.replace('{BASE_DIR}', base_dir_part)\n        data = data.replace('{SECRET_KEY}', secret_key_part)\n        data = data.replace('{USER_MODEL}', user_model_part)\n        data = data.replace('{AUTHENTICATION}', authentication_part)\n        data = data.replace('{DATABASE}', database_part)\n        data = data.replace('{REDIS}', redis_part)\n\n        data = data.replace('{PROJECT_NAME}', self.project_name)\n        data = data.replace('{PANTHER_VERSION}', version())\n        with Path(path).open('x', encoding='utf-8') as file:\n            file.write(data)\n\n    def collect_creation_data(self):\n        self.progress(0)\n\n        for i, question in enumerate(self.questions):\n            # Clean Question Data\n            field_name = question.pop('field')\n            question['default'] = getattr(self, field_name)\n            is_boolean = question.pop('is_boolean', False)\n            convert_output = str  # Do Nothing\n            if is_boolean:\n                question['message'] += f' (default is {self._to_str(question[\"default\"])})'\n                question['validation_func'] = self._is_boolean\n                question['error_message'] = \"Invalid choice, '{}' not in ['y', 'n']\"\n                convert_output = self._to_boolean\n\n            # Check Question Condition\n            if 'condition' in question and eval(question.pop('condition')) is False:\n                print(flush=True)\n            # Ask Question\n            else:\n                setattr(self, field_name, convert_output(self.ask(**question)))\n            self.progress(i + 1)\n\n    def ask(\n        self,\n        message: str,\n        default: str | bool,\n        error_message: str,\n        validation_func: Callable,\n        show_validation_error: bool = False,\n    ) -> str:\n        value = Prompt.ask(message, console=self.input_console).lower() or default\n        while not validation_func(value):\n            # Remove the last line, show error message and ask again\n            [print(end=self.REMOVE_LAST_LINE, flush=True) for _ in range(message.count('\\n') + 1)]\n            error = validation_func(value, return_error=True) if show_validation_error else value\n            self.console.print(error_message.format(error), style='bold red')\n            value = Prompt.ask(message, console=self.input_console).lower() or default\n            print(end=self.REMOVE_LAST_LINE, flush=True)\n        [print(end=self.REMOVE_LAST_LINE, flush=True) for _ in range(message.count('\\n'))]\n        return value\n\n    def progress(self, step: int, /, extra_rows: int = 0):\n        for i in range(extra_rows + 3 if step else 0):\n            print(self.REMOVE_LAST_LINE, flush=True, end='\\r')\n        self.bar.update(step)\n\n        message = 'Created Successfully' if step == self.progress_len else 'Creating Project'\n        rich_print(f'[b]{message:<21}[/b]', end='', flush=True)\n        rich_print(self.bar, flush=True)\n        print('\\n', flush=True)\n\n    @classmethod\n    def _to_boolean(cls, _input: str) -> bool:\n        return _input in ['y', True]\n\n    @classmethod\n    def _is_boolean(cls, _input: str) -> bool:\n        return _input in ['y', 'n', True, False]\n\n    @classmethod\n    def _to_str(cls, _input: bool) -> str:\n        return 'y' if _input else 'n'\n\n    @classmethod\n    def _check_all_directories(cls, base_directory: str, return_error: bool = False) -> str | bool:\n        \"\"\"Return False or directory_name means that the directory exist.\"\"\"\n        if base_directory != '.' and Path(base_directory).is_dir():\n            return base_directory if return_error else False\n\n        for file_name, data in TEMPLATE.items():\n            sub_directory = f'{base_directory}/{file_name}'\n            if Path(sub_directory).exists():\n                return sub_directory if return_error else False\n\n            if isinstance(data, dict):\n                for sub_file_name in data:\n                    file_path = f'{sub_directory}/{sub_file_name}'\n                    if Path(file_path).exists():\n                        return file_path if return_error else False\n        return True\n\n\ncreate = CreateProject().create\n"
  },
  {
    "path": "panther/cli/create_user_command.py",
    "content": "import importlib.util\nimport os\nimport sys\n\nfrom rich import print as rich_print\nfrom rich.console import Console\nfrom rich.prompt import Prompt\n\nfrom panther.cli.utils import cli_error\nfrom panther.configs import config\nfrom panther.db.models import BaseUser\nfrom panther.utils import run_coroutine\n\n\ndef get_password(prompt: str) -> str:\n    rich_print(f'[b]{prompt}: [/b]', end='', flush=True)\n    password = ''\n\n    if sys.platform == 'win32':\n        import msvcrt\n\n        while True:\n            ch = msvcrt.getch()\n            if ch in (b'\\r', b'\\n'):\n                print()\n                break\n            elif ch == b'\\x08':  # Backspace\n                if password:\n                    password = password[:-1]\n                    sys.stdout.write('\\b \\b')\n                    sys.stdout.flush()\n            elif ch == b'\\x03':  # Ctrl+C\n                raise KeyboardInterrupt\n            else:\n                try:\n                    char = ch.decode()\n                    password += char\n                    sys.stdout.write('*')\n                    sys.stdout.flush()\n                except UnicodeDecodeError:\n                    continue\n    else:\n        import termios\n        import tty\n\n        fd = sys.stdin.fileno()\n        old_settings = termios.tcgetattr(fd)\n        try:\n            tty.setraw(fd)\n            while True:\n                ch = sys.stdin.read(1)\n                if ch in ('\\r', '\\n'):\n                    print()\n                    break\n                elif ch == '\\x7f':  # Backspace\n                    if password:\n                        password = password[:-1]\n                        sys.stdout.write('\\b \\b')\n                        sys.stdout.flush()\n                elif ch == '\\x03':  # Ctrl+C\n                    raise KeyboardInterrupt\n                else:\n                    password += ch\n                    sys.stdout.write('*')\n                    sys.stdout.flush()\n        finally:\n            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)\n\n    return password\n\n\ndef get_username(prompt: str, user_model: BaseUser, username_field: str):\n    console = Console(style='bold')\n    username = Prompt.ask(prompt=prompt, console=console)\n    while run_coroutine(user_model.exists({username_field: username})):\n        console.print(f'{user_model.__name__} with this {username_field} already exists.', style='bold red')\n        username = Prompt.ask(prompt=prompt, console=console)\n    return username\n\n\ndef load_application_file(file_location: str):\n    file_name = file_location.removesuffix('.py')\n    script_path = os.path.abspath(path=file_location)\n    script_dir = os.path.dirname(script_path)\n    if script_dir not in sys.path:\n        sys.path.insert(0, script_dir)\n    spec = importlib.util.spec_from_file_location(name=file_name, location=file_location)\n    mod = importlib.util.module_from_spec(spec=spec)\n    spec.loader.exec_module(module=mod)\n\n\ndef create_user(args) -> None:\n    if len(args) == 0:\n        return cli_error(\n            'Not Enough Arguments, Give me a file path that contains `Panther()` app.\\n'\n            '       * Make sure to run `panther createuser` in the same directory as that file!\\n'\n            '       * Example: `panther createuser main.py`',\n        )\n    elif len(args) != 1:\n        return cli_error('Too Many Arguments.')\n    load_application_file(file_location=args[0])\n    # Initialization\n    User: BaseUser = config.USER_MODEL\n    user_model_name = User.__name__\n    username_field = User.USERNAME_FIELD\n    console = Console()\n    try:\n        username = get_username(f'Enter your `{username_field}`', user_model=User, username_field=username_field)\n        password = get_password('Enter your `password`')\n    except KeyboardInterrupt:\n        console.print('\\nKeyboard Interrupt', style='bold red')\n        return\n\n    # Creating User\n    user = run_coroutine(User.insert_one({username_field: username}))\n    run_coroutine(user.set_password(password=password))\n    console.print(f'\\n{user_model_name}({username_field}={username}) Created Successfully.', style='bold green')\n"
  },
  {
    "path": "panther/cli/main.py",
    "content": "import logging\nimport sys\n\nfrom panther import version as panther_version\nfrom panther.cli.create_project_command import create\nfrom panther.cli.create_user_command import create_user\nfrom panther.cli.monitor_command import monitor\nfrom panther.cli.run_command import run\nfrom panther.cli.utils import cli_error, print_help_message\n\nlogger = logging.getLogger('panther')\n\n\ndef shell(args) -> None:\n    if len(args) == 0:\n        return cli_error(\n            'Not Enough Arguments, Give me a file path that contains `Panther()` app.\\n'\n            '       * Make sure to run `panther shell` in the same directory as that file!\\n'\n            '       * Example: `panther shell main.py`',\n        )\n    elif len(args) != 1:\n        return cli_error('Too Many Arguments.')\n\n    package = args[0].removesuffix('.py')\n    try:\n        from IPython import start_ipython\n\n        start_ipython(('--gui', 'asyncio', '-c', f'\"import {package}\"', '-i'))\n    except ImportError:\n        logger.error('Make sure `ipython` is installed -> Hint: `pip install ipython`')\n\n\ndef version() -> None:\n    print(panther_version())\n\n\ndef start() -> None:\n    args = sys.argv[2:]\n    match len(sys.argv) > 1 and sys.argv[1]:\n        case 'h' | 'help' | '-h' | '--help':\n            print_help_message()\n        case 'create':\n            create(args)\n        case 'createuser':\n            create_user(args)\n        case 'run':\n            run(args)\n        case 'shell':\n            shell(args)\n        case 'monitor':\n            monitor()\n        case 'version' | '--version':\n            version()\n        case _:\n            cli_error('Invalid Arguments.')\n"
  },
  {
    "path": "panther/cli/monitor_command.py",
    "content": "import contextlib\nimport logging\nimport os\nimport platform\nimport signal\nfrom collections import deque\nfrom pathlib import Path\n\nfrom rich import box\nfrom rich.align import Align\nfrom rich.console import Group\nfrom rich.live import Live\nfrom rich.panel import Panel\nfrom rich.table import Table\n\nfrom panther.cli.utils import import_error\nfrom panther.configs import config\nfrom panther.middlewares.monitoring import WebsocketMonitoringMiddleware\n\nwith contextlib.suppress(ImportError):\n    from watchfiles import watch\n\nlogger = logging.getLogger('panther')\n\n\nclass Monitoring:\n    def __init__(self):\n        self.rows = deque()\n        self.monitoring_log_file = Path(config.BASE_DIR / 'logs' / 'monitoring.log')\n\n    def monitor(self) -> None:\n        if error := self.initialize():\n            # Don't continue if initialize() has error\n            logger.error(error)\n            return\n\n        with (\n            self.monitoring_log_file.open() as f,\n            Live(\n                self.generate_table(),\n                vertical_overflow='visible',\n                screen=True,\n            ) as live,\n            contextlib.suppress(KeyboardInterrupt),\n        ):\n            f.readlines()  # Set cursor at the end of the file\n\n            if platform.system() == 'Windows':\n                watching = watch(self.monitoring_log_file, force_polling=True)\n            else:\n                watching = watch(self.monitoring_log_file)\n\n            for _ in watching:\n                for line in f.readlines():\n                    # line = datetime | method | path | ip:port | response_time(seconds) | status\n                    columns = line.split('|')\n                    if len(columns) != 2:  # Can be `datetime | ` on initiation.\n                        columns[4] = self._clean_response_time(columns[4])\n                        self.rows.append(columns)\n                        live.update(self.generate_table())\n\n    def initialize(self) -> str:\n        # Check requirements\n        try:\n            from watchfiles import watch\n        except ImportError as e:\n            return import_error(e, package='watchfiles').args[0]\n\n        # Check log file\n        if not self.monitoring_log_file.exists():\n            return (\n                f'`{self.monitoring_log_file}` file not found. '\n                f'Make sure `panther.middlewares.monitoring.MonitoringMiddleware` is in your `MIDDLEWARES`.\\n'\n                f'documentation: https://PantherPy.github.io/middlewares/#monitoring-middleware'\n            )\n\n        # Initialize Deque\n        self.update_rows()\n\n        # Register the signal handler\n        if platform.system() != 'Windows':\n            signal.signal(signal.SIGWINCH, self.update_rows)\n\n    def generate_table(self) -> Panel:\n        # 2023-03-24 01:42:52 | GET | /user/317/ | 127.0.0.1:48856 |  0.0366 ms | 200\n\n        table = Table(box=box.MINIMAL_DOUBLE_HEAD)\n        table.add_column('Datetime', justify='center', style='magenta', no_wrap=True)\n        table.add_column('Method', justify='center', style='cyan', no_wrap=True)\n        table.add_column('Path', justify='center', style='cyan', no_wrap=True)\n        table.add_column('Client', justify='center', style='cyan')\n        table.add_column('Response Time', justify='center', style='blue')\n        table.add_column('Status', justify='center', style='blue', no_wrap=True)\n\n        for row in self.rows:\n            table.add_row(*row)\n\n        return Panel(\n            Align.center(Group(table)),\n            box=box.ROUNDED,\n            padding=(0, 2),\n            title='Monitoring',\n            border_style='bright_blue',\n        )\n\n    def update_rows(self, *args, **kwargs):\n        # Top = -4, Bottom = -2 --> -6\n        # Print of each line needs two line, so --> x // 2\n        lines = (os.get_terminal_size()[1] - 6) // 2\n        self.rows = deque(self.rows, maxlen=lines)\n\n    @classmethod\n    def _clean_response_time(cls, response_time: str) -> str:\n        if response_time == WebsocketMonitoringMiddleware.ConnectedConnectionTime:\n            return response_time\n        response_time = float(response_time)\n        time_unit = ' s'\n\n        if response_time < 0.01:\n            response_time = response_time * 1_000\n            time_unit = 'ms'\n\n        elif response_time >= 60:\n            response_time = response_time / 60\n            time_unit = ' m'\n\n        return f'{round(response_time, 4)} {time_unit}'\n\n\nmonitor = Monitoring().monitor\n"
  },
  {
    "path": "panther/cli/run_command.py",
    "content": "import contextlib\nimport sys\n\nimport uvicorn\n\nfrom panther.cli.utils import cli_error\n\n\ndef run(args: list[str]) -> None:\n    try:\n        with contextlib.suppress(KeyboardInterrupt):\n            # First arg will be ignored by @Click, so ...\n            sys.argv = ['main'] + args\n            uvicorn.main()\n    except TypeError as e:\n        cli_error(e)\n"
  },
  {
    "path": "panther/cli/template.py",
    "content": "from datetime import datetime\n\nfrom panther import version\nfrom panther.utils import generate_secret_key\n\napis_py = \"\"\"from datetime import timedelta\n\nfrom panther import status\nfrom panther.app import API\nfrom panther.response import Response\nfrom panther.utils import timezone_now\n\n\n# More Info: https://PantherPy.GitHub.io/api/\n@API(cache=timedelta(minutes=1))\nasync def hello_world_api():\n    current_time = timezone_now().isoformat()\n    return Response(\n        data={'message': f'Hello from Panther! 🐾 | {current_time}'},\n        status_code=status.HTTP_200_OK\n    )\n\"\"\"\n\nmodels_py = \"\"\"\\\"\"\"\nMore Info: https://PantherPy.GitHub.io/model/\n\\\"\"\"\nfrom panther.db import Model\n\"\"\"\n\nserializers_py = \"\"\"\\\"\"\"\nMore Info: https://PantherPy.GitHub.io/serializer/\n\\\"\"\"\nfrom panther.serializer import ModelSerializer\n\"\"\"\n\nmiddlewares_py = \"\"\"\\\"\"\"\nMore Info: https://PantherPy.GitHub.io/middlewares/\n\\\"\"\"\n\"\"\"\n\napp_urls_py = \"\"\"from app.apis import hello_world_api\n\nurl_routing = {\n    '/': hello_world_api,\n}\n\"\"\"\n\nconfigs_py = \"\"\"\\\"\"\"\n{PROJECT_NAME} Project (Generated by Panther on %s)\n\\\"\"\"{IMPORT_PATH}{IMPORT_COMPLETE_LOAD_ENV}\n{BASE_DIR}{SECRET_KEY}{DATABASE}{REDIS}{USER_MODEL}{AUTHENTICATION}\n\n# More Info: https://PantherPy.GitHub.io/timezone/\nTIMEZONE = 'UTC'\n\n# More Info: https://PantherPy.GitHub.io/urls/\nURLs = 'core.urls.url_routing'\n\"\"\" % datetime.now().date().isoformat()\n\nenv = \"\"\"SECRET_KEY='%s'\n\"\"\" % generate_secret_key()\n\nmain_py = \"\"\"from panther import Panther\n\napp = Panther(__name__)\n\"\"\"\n\nurls_py = \"\"\"from app.urls import url_routing as app_url_routing\n\nurl_routing = {\n    '/': app_url_routing,\n}\n\"\"\"\n\ngit_ignore = \"\"\"__pycache__/\n.venv/\n.idea/\nlogs/\n\n.env\n*.pdb\n\"\"\"\n\nrequirements = \"\"\"panther==%s\n\"\"\" % version()\n\nTEMPLATE = {\n    'app': {\n        '__init__.py': '',\n        'apis.py': apis_py,\n        'models.py': models_py,\n        'serializers.py': serializers_py,\n        'urls.py': app_urls_py,\n    },\n    'core': {\n        '__init__.py': '',\n        'configs.py': configs_py,\n        'middlewares.py': middlewares_py,\n        'urls.py': urls_py,\n    },\n    'main.py': main_py,\n    '.env': env,\n    '.gitignore': git_ignore,\n    'requirements.txt': requirements,\n}\n\nsingle_main_py = \"\"\"\\\"\"\"\n{PROJECT_NAME} Project (Generated by Panther on %s)\n\\\"\"\"\nfrom datetime import timedelta{IMPORT_PATH}\n\nfrom panther import Panther, status\nfrom panther.app import API\nfrom panther.response import Response\nfrom panther.utils import {IMPORT_LOAD_ENV}timezone_now\n{BASE_DIR}{SECRET_KEY}{DATABASE}{REDIS}{USER_MODEL}{AUTHENTICATION}\n\n# More Info: https://PantherPy.GitHub.io/timezone/\nTIMEZONE = 'UTC'\n\n\n# More Info: https://PantherPy.GitHub.io/api/\n@API(cache=timedelta(minutes=1))\nasync def hello_world_api():\n    current_time = timezone_now().isoformat()\n    return Response(\n        data={'message': f'Hello from Panther! 🐾 | {current_time}'},\n        status_code=status.HTTP_200_OK\n    )\n\n\nurl_routing = {\n    '/': hello_world_api,\n}\n\napp = Panther(__name__, configs=__name__, urls=url_routing)\n\"\"\" % datetime.now().date().isoformat()\n\nSINGLE_FILE_TEMPLATE = {\n    'main.py': single_main_py,\n    '.env': env,\n    '.gitignore': git_ignore,\n    'requirements.txt': requirements,\n}\n\nDATABASE_PANTHERDB_PART = \"\"\"\n\n# More Info: https://PantherPy.GitHub.io/database/#pantherdb\nDATABASE = {\n    'engine': {\n        'class': 'panther.db.connections.PantherDBConnection',\n        'path': BASE_DIR / 'database.pdb',\n    }\n}\"\"\"\n\nDATABASE_MONGODB_PART = \"\"\"\n\n# More Info: https://PantherPy.GitHub.io/database/#mongodb\nDATABASE = {\n    'engine': {\n        'class': 'panther.db.connections.MongoDBConnection',\n        'host': '127.0.0.1',\n        'port': 27017,\n        'database': '{PROJECT_NAME}'\n    }\n}\"\"\"\n\nREDIS_PART = \"\"\"\n\n# More Info: https://PantherPy.GitHub.io/redis/\nREDIS = {\n    'class': 'panther.db.connections.RedisConnection',\n    'host': '127.0.0.1',\n    'port': 6379,\n}\"\"\"\n\nUSER_MODEL_PART = \"\"\"\n\n# More Info: https://PantherPy.GitHub.io/user_model/\nUSER_MODEL = 'panther.db.models.BaseUser'\"\"\"\n\nAUTHENTICATION_PART = \"\"\"\n\n# More Info: https://PantherPy.GitHub.io/authentications/\nAUTHENTICATION = 'panther.authentications.JWTAuthentication'\"\"\"\n\nSECRET_KEY_PART = \"\"\"\n\nenv = load_env(BASE_DIR / '.env')\n\nSECRET_KEY = env['SECRET_KEY']\"\"\"\n\nBASE_DIR_PART = \"\"\"\nBASE_DIR = Path(__name__).resolve().parent\"\"\"\n\nIMPORT_PATH_PART = \"\"\"\nfrom pathlib import Path\"\"\"\n\nIMPORT_LOAD_ENV_PART = \"\"\"load_env, \"\"\"\n\nIMPORT_COMPLETE_LOAD_ENV_PART = \"\"\"\nfrom panther.utils import load_env\"\"\"\n"
  },
  {
    "path": "panther/cli/utils.py",
    "content": "import logging\nimport platform\n\nfrom rich import print as rprint\n\nfrom panther.configs import Config\nfrom panther.exceptions import PantherError\n\nlogger = logging.getLogger('panther')\n\nif platform.system() == 'Windows':\n    h = '|'\n    v = '_'\n    tr = ' '\n    tl = ' '\n    br = ' '\n    bl = ' '\nelse:\n    h = '│'\n    v = '─'\n    tr = '╮'\n    tl = '╭'\n    br = '╯'\n    bl = '╰'\n\ntop = f'{tl}{60 * v}{tr}'\nbottom = f'{bl}{60 * v}{br}'\n\nlogo = rf\"\"\"{top}\n{h}     ____                 __    __                          {h}\n{h}    /\\  _`\\              /\\ \\__/\\ \\                         {h}\n{h}    \\ \\ \\L\\ \\ __      ___\\ \\ ,_\\ \\ \\___      __   _ __      {h}\n{h}     \\ \\ ,__/'__`\\  /' _ `\\ \\ \\/\\ \\  _ `\\  /'__`\\/\\`'__\\    {h}\n{h}      \\ \\ \\/\\ \\L\\.\\_/\\ \\/\\ \\ \\ \\_\\ \\ \\ \\ \\/\\  __/\\ \\ \\/     {h}\n{h}       \\ \\_\\ \\__/.\\_\\ \\_\\ \\_\\ \\__\\\\ \\_\\ \\_\\ \\____\\\\ \\_\\     {h}\n{h}        \\/_/\\/__/\\/_/\\/_/\\/_/\\/__/ \\/_/\\/_/\\/____/ \\/_/     {h}\n{h}                                                            {h}\"\"\"\n\nhelp_message = rf\"\"\"{logo}\n{h}   Usage: panther <command> \\[options]                       {h}\n{h}                                                            {h}\n{h}   Commands:                                                {h}\n{h}       - create \\[project_name] \\[directory]                  {h}\n{h}           Create a new Panther project.                    {h}\n{h}           * Interactive mode if no arguments provided.     {h}\n{h}           * Non-interactive if project_name and directory  {h}\n{h}             are specified (default directory: .).          {h}\n{h}           Example:                                         {h}\n{h}               - `panther create`                           {h}\n{h}               - `panther create myapp myapp`               {h}\n{h}                                                            {h}\n{h}       - run <app> \\[options]                                {h}\n{h}           Run your Panther project using Uvicorn.          {h}\n{h}           * app: address of your application.              {h}\n{h}           * options: Check `uvicorn --help` for options.   {h}\n{h}           * `panther run` is alias of `uvicorn`.           {h}\n{h}           Example: `panther run main:app --reload`         {h}\n{h}                                                            {h}\n{h}       - shell <application_file>                           {h}       \n{h}           Start an interactive Python shell with your app. {h}\n{h}           * application_file: path to your main app file.  {h}           \n{h}           Example: `panther shell main.py`                 {h}\n{h}                                                            {h}\n{h}       - createuser <application_file>                      {h}       \n{h}           Create a new user based on USER_MODEl from your, {h}\n{h}           configs. (default is panther.db.models.BaseUser) {h}\n{h}           * application_file: path to your main app file.  {h}           \n{h}           Example: `panther createuser main.py`            {h}\n{h}                                                            {h}\n{h}       - monitor                                            {h}\n{h}           Display real-time request monitoring.            {h}\n{h}                                                            {h}\n{h}       - version | --version                                {h}\n{h}           Display the current version of Panther.          {h}\n{h}                                                            {h}\n{h}       - help | h | --help | -h                             {h}\n{h}           Show this help message and exit.                 {h}\n{bottom}\n\"\"\"\n\n\ndef import_error(message: str | Exception, package: str | None = None) -> PantherError:\n    msg = str(message)\n    if package:\n        msg += f' -> Hint: `pip install {package}`'\n    return PantherError(msg)\n\n\ndef cli_error(message: str | Exception) -> None:\n    logger.error(message)\n    logger.info('Use \"panther -h\" for more help')\n\n\ndef cli_warning(message: str | Exception, hint: str = None) -> None:\n    logger.warning(message)\n    if hint:\n        logger.info(hint)\n\n\ndef cli_info(message: str) -> None:\n    logger.info(message)\n    logger.info('Use \"panther -h\" for more help\\n')\n\n\ndef print_help_message():\n    rprint(help_message)\n\n\ndef print_info(config: Config):\n    from panther.db.connections import redis\n\n    mo = config.MONITORING\n    lq = config.LOG_QUERIES\n    bt = config.BACKGROUND_TASKS\n    ws = config.HAS_WS\n    rd = redis.is_connected\n    bd = f'{config.BASE_DIR!s:<41}'\n    if len(bd) > 41:\n        bd = f'{bd[:38]}...'\n\n    # Monitoring\n    monitor = f'{h} * Run \"panther monitor\" in another session for Monitoring  {h}\\n' if config.MONITORING else None\n\n    # Uvloop\n    uvloop_msg = None\n    if platform.system() != 'Windows':\n        try:\n            import uvloop\n        except ImportError:\n            uvloop_msg = (\n                f'{h} * You may want to install `uvloop` for better performance  {h}\\n'\n                f'{h}   `pip install uvloop`                                     {h}\\n'\n            )\n\n    # Gunicorn if Websocket\n    gunicorn_msg = None\n    if config.HAS_WS:\n        try:\n            import gunicorn\n\n            gunicorn_msg = f'{h} * You have WS, so make sure to run gunicorn with --preload  {h}\\n'\n        except ImportError:\n            pass\n\n    # Message\n    info_message = f\"\"\"{logo}\n{h}   Redis: {rd}                                       \\t     {h}\n{h}   Websocket: {ws}                                   \\t     {h}\n{h}   Monitoring: {mo}                                  \\t     {h}\n{h}   Log Queries: {lq}                                 \\t     {h}\n{h}   Background Tasks: {bt}                            \\t     {h}\n{h}   Base directory: {bd}{h}\n\"\"\"\n    if monitor:\n        info_message += monitor\n    if uvloop_msg:\n        info_message += uvloop_msg\n    if gunicorn_msg:\n        info_message += gunicorn_msg\n\n    info_message += bottom\n    rprint(info_message)\n"
  },
  {
    "path": "panther/configs.py",
    "content": "import typing\nfrom collections.abc import Callable\nfrom dataclasses import dataclass, field\nfrom datetime import timedelta\nfrom pathlib import Path\n\nimport jinja2\nfrom pydantic import BaseModel as PydanticBaseModel\n\n\nclass JWTConfig:\n    def __init__(\n        self,\n        key: str,\n        algorithm: str = 'HS256',\n        life_time: timedelta | int = timedelta(days=1),\n        refresh_life_time: timedelta | int | None = None,\n    ):\n        self.key = key\n        self.algorithm = algorithm\n        self.life_time = int(life_time.total_seconds()) if isinstance(life_time, timedelta) else life_time\n\n        if refresh_life_time:\n            if isinstance(refresh_life_time, timedelta):\n                self.refresh_life_time = refresh_life_time.total_seconds()\n            else:\n                self.refresh_life_time = refresh_life_time\n        else:\n            self.refresh_life_time = self.life_time * 2\n\n    def __eq__(self, other):\n        return bool(\n            self.key == other.key\n            and self.algorithm == other.algorithm\n            and self.life_time == other.life_time\n            and self.refresh_life_time == other.refresh_life_time,\n        )\n\n\nclass QueryObservable:\n    observers = []\n\n    @classmethod\n    def observe(cls, observer):\n        cls.observers.append(observer)\n\n    @classmethod\n    def update(cls):\n        for observer in cls.observers:\n            observer._reload_bases(parent=config.QUERY_ENGINE)\n\n\n@dataclass\nclass Config:\n    BASE_DIR: Path = Path()\n    MONITORING: bool = False\n    LOG_QUERIES: bool = False\n    THROTTLING = None  # type: panther.throttling.Throttle\n    SECRET_KEY: str | None = None\n    HTTP_MIDDLEWARES: list = field(default_factory=list)  # Middlewares stored in reversed order\n    WS_MIDDLEWARES: list = field(default_factory=list)  # Middlewares stored in reversed order\n    USER_MODEL: type[PydanticBaseModel] | None = None\n    AUTHENTICATION: type[PydanticBaseModel] | None = None\n    WS_AUTHENTICATION: type[PydanticBaseModel] | None = None\n    JWT_CONFIG: JWTConfig | None = None\n    MODELS: list = field(default_factory=list)\n    FLAT_URLS: dict = field(default_factory=dict)\n    URLS: dict = field(default_factory=dict)\n    WEBSOCKET_CONNECTIONS: Callable | None = None\n    BACKGROUND_TASKS: bool = False\n    HAS_WS: bool = False\n    TIMEZONE: str = 'UTC'\n    TEMPLATES_DIR: str | list[str] = '.'\n    JINJA_ENVIRONMENT: jinja2.Environment | None = None\n    AUTO_REFORMAT: bool = False\n    QUERY_ENGINE: Callable | None = None\n    DATABASE: Callable | None = None\n\n    def refresh(self):\n        \"\"\"\n        Reset built-in fields and remove any custom (non-built-in) attributes.\n        * In some tests we need to `refresh` the `config` values\n        \"\"\"\n        builtin_fields = set(self.__dataclass_fields__)\n        current_fields = set(self.__dict__)\n\n        # Reset built-in fields\n        for field_name in builtin_fields:\n            field_def = self.__dataclass_fields__[field_name]\n            default = field_def.default_factory() if callable(field_def.default_factory) else field_def.default\n            setattr(self, field_name, default)\n\n        # Delete custom attributes\n        for field_name in current_fields - builtin_fields:\n            delattr(self, field_name)\n\n    def vars(self) -> dict[str, typing.Any]:\n        \"\"\"Return all config variables (built-in + custom).\"\"\"\n        return dict(self.__dict__)\n\n    def __setattr__(self, key, value):\n        super().__setattr__(key, value)\n        if key == 'QUERY_ENGINE' and value:\n            QueryObservable.update()\n\n    def __getattr__(self, item: str):\n        try:\n            return object.__getattribute__(self, item)\n        except AttributeError:\n            return None\n\n    def __setitem__(self, key, value):\n        setattr(self, key.upper(), value)\n\n    def __getitem__(self, item):\n        return getattr(self, item.upper())\n\n\nconfig = Config()\n"
  },
  {
    "path": "panther/db/__init__.py",
    "content": "from panther.db.models import Model  # noqa: F401\n"
  },
  {
    "path": "panther/db/connections.py",
    "content": "import asyncio\nimport contextlib\nfrom abc import abstractmethod\nfrom typing import TYPE_CHECKING, Any\n\nfrom pantherdb import PantherDB\n\nfrom panther.cli.utils import import_error\nfrom panther.configs import config\nfrom panther.utils import Singleton\n\ntry:\n    from redis.asyncio import Redis as _Redis\nexcept ImportError:\n    # This '_Redis' is not going to be used,\n    #   If user really wants to use redis,\n    #   we are going to force him to install it in `panther._load_configs.load_redis`\n    _Redis = type('_Redis', (), {'__new__': lambda x: x})\n\nif TYPE_CHECKING:\n    from pymongo.database import Database\n\n\nclass BaseDatabaseConnection:\n    def __init__(self, *args, **kwargs):\n        \"\"\"Initialized in application startup\"\"\"\n        self.init(*args, **kwargs)\n\n    @abstractmethod\n    def init(self, *args, **kwargs):\n        pass\n\n    @property\n    @abstractmethod\n    def session(self):\n        pass\n\n\nclass MongoDBConnection(BaseDatabaseConnection):\n    def init(\n        self,\n        host: str = 'localhost',\n        port: int = 27017,\n        document_class: dict[str, Any] | None = None,\n        tz_aware: bool | None = None,\n        connect: bool | None = None,\n        type_registry=None,  # type: bson.codec_options.TypeRegistry\n        database: str | None = None,\n        **kwargs: Any,\n    ) -> None:\n        try:\n            from motor.motor_asyncio import AsyncIOMotorClient\n        except ModuleNotFoundError as e:\n            raise import_error(e, package='motor')\n\n        with contextlib.suppress(ImportError):\n            import uvloop\n\n            asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())\n\n        self._client: AsyncIOMotorClient = AsyncIOMotorClient(\n            host=host,\n            port=port,\n            document_class=document_class,\n            tz_aware=tz_aware,\n            connect=connect,\n            type_registry=type_registry,\n            **kwargs,\n        )\n        self._database: Database = self._client.get_database(name=database)\n\n    @property\n    def session(self):\n        return self._database\n\n    @property\n    def client(self):\n        return self._client\n\n\nclass PantherDBConnection(BaseDatabaseConnection):\n    def init(self, path: str | None = None, encryption: bool = False):\n        params = {'db_name': path, 'return_dict': True, 'return_cursor': True}\n        if encryption:\n            try:\n                import cryptography\n            except ImportError as e:\n                raise import_error(e, package='cryptography')\n            params['secret_key'] = config.SECRET_KEY.encode()\n\n        self._connection: PantherDB = PantherDB(**params)\n\n    @property\n    def session(self):\n        return self._connection\n\n    @property\n    def client(self):\n        return self._connection\n\n\nclass DatabaseConnection(Singleton):\n    @property\n    def session(self):\n        return config.DATABASE.session\n\n    @property\n    def is_defined(self):\n        return bool(config.DATABASE)\n\n    @property\n    def client(self):\n        return config.DATABASE.client\n\n\nclass RedisConnection(Singleton, _Redis):\n    is_connected: bool = False\n\n    def __init__(\n        self,\n        init: bool = False,\n        host: str = 'localhost',\n        port: int = 6379,\n        db: int = 0,\n        **kwargs,\n    ):\n        if init:\n            self.host = host\n            self.port = port\n            self.db = db\n            self.kwargs = kwargs\n\n            super().__init__(host=host, port=port, db=db, **kwargs)\n            self.is_connected = True\n            self.sync_ping()\n\n    def sync_ping(self):\n        from redis import Redis\n\n        Redis(host=self.host, port=self.port, socket_timeout=3, **self.kwargs).ping()\n\n    async def execute_command(self, *args, **options):\n        if self.is_connected:\n            return await super().execute_command(*args, **options)\n        msg = '`REDIS` is not found in `configs`'\n        raise ValueError(msg)\n\n    def create_connection_for_websocket(self) -> _Redis:\n        if not hasattr(self, 'websocket_connection'):\n            self.websocket_connection = _Redis(host=self.host, port=self.port, db=0, **self.kwargs)\n        return self.websocket_connection\n\n\ndb: DatabaseConnection = DatabaseConnection()\nredis: RedisConnection = RedisConnection()\n"
  },
  {
    "path": "panther/db/cursor.py",
    "content": "from __future__ import annotations\n\nfrom sys import version_info\n\nfrom panther.utils import run_coroutine\n\ntry:\n    from pymongo.cursor import Cursor as _Cursor\nexcept ImportError:\n    # This '_Cursor' is not going to be used,\n    #   If user really wants to use it,\n    #   we are going to force him to install it in `panther.db.connections.MongoDBConnection.init`\n    _Cursor = type('_Cursor', (), {})\n\nif version_info >= (3, 11):\n    from typing import Self\nelse:\n    from typing import TypeVar\n\n    Self = TypeVar('Self', bound='BaseMongoDBQuery')\n\n\nclass Cursor(_Cursor):\n    models = {}\n\n    def __init__(self, collection, *args, cls=None, **kwargs):\n        # cls.__name__ and collection.name are equal.\n        if cls:\n            self.models[collection.name] = cls\n            self.cls = cls\n            self.filter = kwargs['filter']\n        else:\n            self.cls = self.models[collection.name]\n        super().__init__(collection, *args, **kwargs)\n\n    def __aiter__(self) -> Self:\n        return self\n\n    def __iter__(self) -> Self:\n        return self\n\n    async def next(self) -> Self:\n        return await self.cls._create_model_instance(document=super().next())\n\n    async def __anext__(self) -> Self:\n        try:\n            return await self.cls._create_model_instance(document=super().next())\n        except StopIteration:\n            raise StopAsyncIteration\n\n    def __next__(self) -> Self:\n        try:\n            return run_coroutine(self.cls._create_model_instance(document=super().next()))\n        except StopIteration:\n            raise\n\n    def __getitem__(self, index: int | slice) -> Cursor[Self] | Self:\n        document = super().__getitem__(index)\n        if isinstance(document, dict):\n            return run_coroutine(self.cls._create_model_instance(document=document))\n        return document\n"
  },
  {
    "path": "panther/db/models.py",
    "content": "import contextlib\nimport os\nimport sys\nfrom datetime import datetime\nfrom typing import Annotated, ClassVar\n\nfrom pydantic import BaseModel as PydanticBaseModel\nfrom pydantic import Field, PlainSerializer, WrapValidator\n\nfrom panther.configs import config\nfrom panther.db.queries import Query\nfrom panther.utils import URANDOM_SIZE, scrypt, timezone_now\n\nwith contextlib.suppress(ImportError):\n    # Only required if user wants to use mongodb\n    import bson\n\nif sys.version_info >= (3, 11):\n    from typing import Self\nelse:\n    from typing import TypeVar\n\n    Self = TypeVar('Self', bound='BaseUser')\n\n\ndef validate_object_id(value, handler):\n    if config.DATABASE.__class__.__name__ != 'MongoDBConnection':\n        return str(value)\n\n    if isinstance(value, bson.ObjectId):\n        return value\n\n    try:\n        return bson.ObjectId(value)\n    except Exception as e:\n        msg = 'Invalid ObjectId'\n        raise ValueError(msg) from e\n\n\nID = Annotated[str, WrapValidator(validate_object_id), PlainSerializer(lambda x: str(x), return_type=str)] | None\n\n\nclass Model(PydanticBaseModel, Query):\n    def __init_subclass__(cls, **kwargs):\n        if cls.__module__ == 'panther.db.models' and cls.__name__ == 'BaseUser':\n            return\n        config.MODELS.append(cls)\n\n    id: ID = None\n\n    @property\n    def _id(self):\n        \"\"\"\n        Returns the actual ID value:\n            - For MongoDB: returns ObjectId\n            - For PantherDB: returns str\n        \"\"\"\n        if config.DATABASE.__class__.__name__ == 'MongoDBConnection':\n            return bson.ObjectId(self.id)\n        return self.id\n\n\nclass BaseUser(Model):\n    username: str\n    password: str = Field('', max_length=64)\n    last_login: datetime | None = None\n    date_created: datetime | None = None\n\n    USERNAME_FIELD: ClassVar = 'username'\n\n    @classmethod\n    def insert_one(cls, _document: dict | None = None, /, **kwargs) -> Self:\n        kwargs['date_created'] = timezone_now()\n        return super().insert_one(_document, **kwargs)\n\n    async def login(self) -> dict:\n        \"\"\"Return dict of access and refresh tokens\"\"\"\n        await self.update(last_login=timezone_now())\n        return await config.AUTHENTICATION.login(user=self)\n\n    async def refresh_tokens(self) -> dict:\n        \"\"\"Return dict of new access and refresh tokens\"\"\"\n        return await config.AUTHENTICATION.refresh(user=self)\n\n    async def logout(self) -> dict:\n        return await config.AUTHENTICATION.logout(user=self)\n\n    async def set_password(self, password: str):\n        \"\"\"\n        URANDOM_SIZE = 16 char -->\n            salt = 16 bytes\n            salt.hex() = 32 char\n            derived_key = 32 char\n        \"\"\"\n        salt = os.urandom(URANDOM_SIZE)\n        derived_key = scrypt(password=password, salt=salt, digest=True)\n\n        hashed_password = f'{salt.hex()}{derived_key}'\n        await self.update(password=hashed_password)\n\n    def check_password(self, password: str) -> bool:\n        size = URANDOM_SIZE * 2\n        salt = self.password[:size]\n        stored_hash = self.password[size:]\n        derived_key = scrypt(password=password, salt=bytes.fromhex(salt), digest=True)\n\n        return derived_key == stored_hash\n"
  },
  {
    "path": "panther/db/queries/__init__.py",
    "content": "from panther.db.queries.queries import Query\n"
  },
  {
    "path": "panther/db/queries/base_queries.py",
    "content": "from __future__ import annotations\n\nimport operator\nimport types\nimport typing\nfrom abc import abstractmethod\nfrom collections.abc import Iterator\nfrom datetime import datetime\nfrom functools import reduce\nfrom sys import version_info\nfrom typing import Any, Union, get_args, get_origin\n\nfrom pantherdb import Cursor\nfrom pydantic import BaseModel, ValidationError\nfrom pydantic_core._pydantic_core import ValidationError\n\nfrom panther._utils import detect_mime_type\nfrom panther.db.cursor import Cursor\nfrom panther.db.utils import prepare_id_for_query\nfrom panther.exceptions import DatabaseError\nfrom panther.file_handler import File\n\nif version_info >= (3, 11):\n    from typing import Self\nelse:\n    from typing import TypeVar\n\n    Self = TypeVar('Self', bound='BaseQuery')\n\n\nclass BaseQuery:\n    @classmethod\n    def _merge(cls, *args, is_mongo: bool = False) -> dict:\n        prepare_id_for_query(*args, is_mongo=is_mongo)\n        return reduce(operator.ior, filter(None, args), {})\n\n    @classmethod\n    def _clean_error_message(cls, validation_error: ValidationError, is_updating: bool = False) -> str:\n        error = ', '.join(\n            '{field}=\"{error}\"'.format(field='.'.join(str(loc) for loc in e['loc']), error=e['msg'])\n            for e in validation_error.errors()\n            if not is_updating or e['type'] != 'missing'\n        )\n        return f'{cls.__name__}({error})' if error else ''\n\n    @classmethod\n    def _validate_data(cls, *, data: dict, is_updating: bool = False):\n        \"\"\"Validate document before inserting to collection\"\"\"\n        try:\n            cls(**data)\n        except ValidationError as validation_error:\n            if error := cls._clean_error_message(validation_error=validation_error, is_updating=is_updating):\n                raise DatabaseError(error)\n\n    @classmethod\n    def _get_annotation_type(cls, annotation: Any) -> type | None:\n        \"\"\"\n        Extracts the underlying, non-optional type from a type annotation.\n        Handles basic types, Pydantic BaseModels, lists, and unions (optionals).\n        Returns None if no single underlying type can be determined (e.g., for list[NoneType]).\n        Raises DatabaseError for unsupported annotations.\n        \"\"\"\n        origin = get_origin(annotation)\n\n        # Handle list[T] and Union[T, None] (T | None or typing.Union[T, None])\n        if origin is list or origin is types.UnionType or origin is Union:\n            # Extracts the first non-None type from a tuple of type arguments.\n            for arg in get_args(annotation):\n                if arg is not type(None):\n                    return arg\n            return None\n\n        # Handle basic types (str, int, bool, dict, datetime) and Pydantic.BaseModel and File subclasses\n        try:\n            if isinstance(annotation, type) and (\n                annotation in (str, int, bool, dict, float, datetime) or issubclass(annotation, (BaseModel, File))\n            ):\n                return annotation\n        except TypeError:\n            # issubclass(dict[str, tests.test_database_advance.Book], BaseModel) in Python3.10 gives us TypeError and\n            # works just fine in other versions, so we have to raise DatabaseError in this situation anyway.\n            pass\n        raise DatabaseError(f'Panther does not support {annotation} as a field type for unwrapping.')\n\n    @classmethod\n    async def _create_list(cls, field_type: type, value: Any) -> Any:\n        from panther.db import Model\n\n        # `field_type` is the expected type of items in the list (e.g., int, Model, list[str])\n        # `value` is a single item from the input list that needs processing.\n\n        # Handles list[list[int]], list[dict[str,int]] etc.\n        if isinstance(field_type, (types.GenericAlias, typing._GenericAlias)):\n            element_type = cls._get_annotation_type(field_type)  # Unwrap further (e.g. list[str] -> str)\n            if element_type is None:\n                raise DatabaseError(f'Cannot determine element type for generic list item: {field_type}')\n            if not isinstance(value, list):  # Or check if iterable, matching the structure\n                raise DatabaseError(f'Expected a list for nested generic type {field_type}, got {type(value)}')\n            return [await cls._create_list(field_type=element_type, value=item) for item in value]\n\n        # Make sure Model condition is before BaseModel.\n        if isinstance(field_type, type) and issubclass(field_type, Model):\n            # `value` is assumed to be an ID for the Model instance.\n            return await field_type.first(id=value)\n\n        if isinstance(field_type, type) and issubclass(field_type, BaseModel):\n            if not isinstance(value, dict):\n                raise DatabaseError(f'Expected a dictionary for BaseModel {field_type.__name__}, got {type(value)}')\n\n            return {\n                field_name: await cls._create_field(model=field_type, field_name=field_name, value=value[field_name])\n                for field_name in value\n            }\n\n        # `value` is a primitive type (str, int, etc.)\n        return value\n\n    @classmethod\n    async def _create_field(cls, model: type, field_name: str, value: Any) -> Any:\n        from panther.db import Model\n\n        # Handle primary key field directly\n        if field_name == 'id':\n            return value\n\n        if field_name not in model.model_fields:\n            # Field from input data is not defined in the model.\n            # Pydantic's `extra` config on the model will handle this upon instantiation.\n            return value\n\n        field_annotation = model.model_fields[field_name].annotation\n        unwrapped_type = cls._get_annotation_type(field_annotation)\n\n        if unwrapped_type is None:\n            raise DatabaseError(\n                f\"Could not determine a valid underlying type for field '{field_name}' \"\n                f'with annotation {field_annotation} in model {model.__name__}.',\n            )\n\n        if get_origin(field_annotation) is list:\n            # Or check for general iterables if applicable\n            if not isinstance(value, list):\n                raise DatabaseError(\n                    f\"Field '{field_name}' expects a list, got {type(value)} for model {model.__name__}\",\n                )\n            return [await cls._create_list(field_type=unwrapped_type, value=item) for item in value]\n\n        # Condition of `File` should be on top of `BaseModel`\n        if isinstance(unwrapped_type, type) and issubclass(unwrapped_type, File):\n            # `value` is assumed to be the path of the File.\n            content_type = detect_mime_type(file_path=value)\n            with open(value, 'rb') as f:\n                return File(file_name=value, content_type=content_type, file=f.read()).model_dump()\n\n        # Condition of `Model` should be on top of `BaseModel`\n        if isinstance(unwrapped_type, type) and issubclass(unwrapped_type, Model):\n            # `value` is assumed to be an ID for the Model instance.\n            if obj := await unwrapped_type.first(id=value):\n                return obj.model_dump()\n            return None\n\n        if isinstance(unwrapped_type, type) and issubclass(unwrapped_type, BaseModel):\n            if not isinstance(value, dict):\n                raise DatabaseError(\n                    f\"Field '{field_name}' expects a dictionary for BaseModel {unwrapped_type.__name__}, \"\n                    f'got {type(value)} in model {model.__name__}',\n                )\n            return {\n                nested_field_name: await cls._create_field(\n                    model=unwrapped_type,\n                    field_name=nested_field_name,\n                    value=value[nested_field_name],\n                )\n                for nested_field_name in unwrapped_type.model_fields\n                if nested_field_name in value\n            }\n\n        return value\n\n    @classmethod\n    async def _create_model_instance(cls, document: dict, is_updating: bool = False) -> Self:\n        \"\"\"Prepares document and creates an instance of the model.\"\"\"\n        if '_id' in document:\n            document['id'] = document.pop('_id')\n\n        processed_document = {\n            field_name: await cls._create_field(model=cls, field_name=field_name, value=field_value)\n            for field_name, field_value in document.items()\n        }\n        try:\n            return cls(**processed_document)\n        except ValidationError as validation_error:\n            if error := cls._clean_error_message(validation_error=validation_error, is_updating=is_updating):\n                raise DatabaseError(error) from validation_error\n\n    @classmethod\n    async def _clean_value(cls, value: Any) -> dict[str, Any] | list[Any]:\n        from panther.db import Model\n\n        match value:\n            case None:\n                return None\n            case Model() as model:\n                if model.id in [None, '']:\n                    await model.save()\n                # We save full object because user didn't specify the type.\n                return model._id\n            case File() as file:\n                # Write file to disk\n                return file.save()\n            case BaseModel() as model:\n                return {\n                    field_name: await cls._clean_value(value=getattr(model, field_name))\n                    for field_name in model.__class__.model_fields\n                }\n            case dict() as d:\n                return {k: await cls._clean_value(value=v) for k, v in d.items()}\n            case list() as l:\n                return [await cls._clean_value(value=item) for item in l]\n        return value\n\n    @classmethod\n    async def _extract_type(cls, field_name: str) -> Any:\n        if field_name not in cls.model_fields:\n            return None\n        field_annotation = cls.model_fields[field_name].annotation\n        unwrapped_type = cls._get_annotation_type(field_annotation)\n\n        if (\n            get_origin(field_annotation) is list\n            and isinstance(unwrapped_type, type)\n            and issubclass(unwrapped_type, BaseModel)\n        ):\n            return list[unwrapped_type]\n\n        if isinstance(unwrapped_type, type) and issubclass(unwrapped_type, BaseModel):\n            return unwrapped_type\n\n    @classmethod\n    async def _process_document(cls, document):\n        # 1. Check type expected type and cast to that\n        # 2. Check type of field_value and do the stuff (save() or return ._id)\n        processed_document = {}\n        for field_name, field_value in document.items():\n            if field_name in ['id', '_id']:\n                continue\n            field_type = await cls._extract_type(field_name)\n            if field_type:\n                if get_origin(field_type) is list:\n                    cls_type = cls._get_annotation_type(field_type)\n                    value = [cls_type(**v) if isinstance(v, dict) else v for v in field_value]\n                else:\n                    if isinstance(field_value, dict):\n                        value = field_type(**field_value)\n                    else:\n                        value = field_value\n            else:\n                value = field_value\n            processed_document[field_name] = await cls._clean_value(value)\n        return processed_document\n\n    @classmethod\n    @abstractmethod\n    async def find_one(cls, *args, **kwargs) -> Self | None:\n        raise NotImplementedError\n\n    @classmethod\n    @abstractmethod\n    async def find(cls, *args, **kwargs) -> list[Self] | Cursor:\n        raise NotImplementedError\n\n    @classmethod\n    @abstractmethod\n    async def first(cls, *args, **kwargs) -> Self | None:\n        raise NotImplementedError\n\n    @classmethod\n    @abstractmethod\n    async def last(cls, *args, **kwargs):\n        raise NotImplementedError\n\n    @classmethod\n    @abstractmethod\n    async def aggregate(cls, *args, **kwargs) -> Iterator[dict]:\n        raise NotImplementedError\n\n    # # # # # Count # # # # #\n    @classmethod\n    @abstractmethod\n    async def count(cls, *args, **kwargs) -> int:\n        raise NotImplementedError\n\n    # # # # # Insert # # # # #\n    @classmethod\n    @abstractmethod\n    async def insert_one(cls, *args, **kwargs) -> Self:\n        raise NotImplementedError\n\n    @classmethod\n    @abstractmethod\n    async def insert_many(cls, *args, **kwargs) -> list[Self]:\n        raise NotImplementedError\n\n    # # # # # Delete # # # # #\n    @abstractmethod\n    async def delete(self) -> None:\n        raise NotImplementedError\n\n    @classmethod\n    @abstractmethod\n    async def delete_one(cls, *args, **kwargs) -> bool:\n        raise NotImplementedError\n\n    @classmethod\n    @abstractmethod\n    async def delete_many(cls, *args, **kwargs) -> int:\n        raise NotImplementedError\n\n    # # # # # Update # # # # #\n    @abstractmethod\n    async def update(self, *args, **kwargs) -> None:\n        raise NotImplementedError\n\n    @classmethod\n    @abstractmethod\n    async def update_one(cls, *args, **kwargs) -> bool:\n        raise NotImplementedError\n\n    @classmethod\n    @abstractmethod\n    async def update_many(cls, *args, **kwargs) -> int:\n        raise NotImplementedError\n"
  },
  {
    "path": "panther/db/queries/mongodb_queries.py",
    "content": "from __future__ import annotations\n\nimport typing\nfrom sys import version_info\n\nfrom panther.db.connections import db\nfrom panther.db.cursor import Cursor\nfrom panther.db.queries.base_queries import BaseQuery\nfrom panther.db.utils import prepare_id_for_query\n\nif typing.TYPE_CHECKING:\n    from collections.abc import Iterable, Sequence\n\ntry:\n    from bson.codec_options import CodecOptions\n    from pymongo.results import InsertManyResult, InsertOneResult\nexcept ImportError:\n    # MongoDB-related libraries are not required by default.\n    # If the user intends to use MongoDB, they must install the required dependencies explicitly.\n    # This will be enforced in `panther.db.connections.MongoDBConnection.init`.\n    CodecOptions = type('CodecOptions', (), {})\n    InsertOneResult = type('InsertOneResult', (), {})\n    InsertManyResult = type('InsertManyResult', (), {})\n\nif version_info >= (3, 11):\n    from typing import Self\nelse:\n    from typing import TypeVar\n\n    Self = TypeVar('Self', bound='BaseMongoDBQuery')\n\n\nclass BaseMongoDBQuery(BaseQuery):\n    @classmethod\n    def _merge(cls, *args, is_mongo: bool = True) -> dict:\n        return super()._merge(*args, is_mongo=is_mongo)\n\n    # TODO: https://jira.mongodb.org/browse/PYTHON-4192\n    # @classmethod\n    # def collection(cls):\n    #     return db.session.get_collection(name=cls.__name__, codec_options=CodecOptions(document_class=cls))\n\n    # # # # # Find # # # # #\n    @classmethod\n    async def find_one(cls, _filter: dict | None = None, /, **kwargs) -> Self | None:\n        if document := await db.session[cls.__name__].find_one(cls._merge(_filter, kwargs)):\n            return await cls._create_model_instance(document=document)\n        return None\n\n    @classmethod\n    async def find(cls, _filter: dict | None = None, /, **kwargs) -> Cursor:\n        return Cursor(cls=cls, collection=db.session[cls.__name__].delegate, filter=cls._merge(_filter, kwargs))\n\n    @classmethod\n    async def first(cls, _filter: dict | None = None, /, **kwargs) -> Self | None:\n        cursor = await cls.find(_filter, **kwargs)\n        async for result in cursor.sort('_id', 1).limit(-1):\n            return result\n        return None\n\n    @classmethod\n    async def last(cls, _filter: dict | None = None, /, **kwargs) -> Self | None:\n        cursor = await cls.find(_filter, **kwargs)\n        async for result in cursor.sort('_id', -1).limit(-1):\n            return result\n        return None\n\n    @classmethod\n    async def aggregate(cls, pipeline: Sequence[dict]) -> Iterable[dict]:\n        return await db.session[cls.__name__].aggregate(pipeline).to_list(None)\n\n    # # # # # Count # # # # #\n    @classmethod\n    async def count(cls, _filter: dict | None = None, /, **kwargs) -> int:\n        return await db.session[cls.__name__].count_documents(cls._merge(_filter, kwargs))\n\n    # # # # # Insert # # # # #\n    @classmethod\n    async def insert_one(cls, document: dict) -> Self:\n        insert_one_result: InsertOneResult = await db.session[cls.__name__].insert_one(document)\n        return insert_one_result.inserted_id\n\n    @classmethod\n    async def insert_many(cls, documents: Iterable[dict]) -> list[Self]:\n        final_documents = []\n        results = []\n        for document in documents:\n            prepare_id_for_query(document, is_mongo=True)\n            final_document = await cls._process_document(document)\n            final_documents.append(final_document)\n            results.append(await cls._create_model_instance(document=final_document))\n        insert_many_result: InsertManyResult = await db.session[cls.__name__].insert_many(final_documents)\n        for obj, inserted_id in zip(results, insert_many_result.inserted_ids):\n            obj.id = inserted_id\n        return results\n\n    # # # # # Delete # # # # #\n    @classmethod\n    async def delete_one(cls, _filter: dict | None = None, /, **kwargs) -> bool:\n        result = await db.session[cls.__name__].delete_one(cls._merge(_filter, kwargs))\n        return bool(result.deleted_count)\n\n    @classmethod\n    async def delete_many(cls, _filter: dict | None = None, /, **kwargs) -> int:\n        result = await db.session[cls.__name__].delete_many(cls._merge(_filter, kwargs))\n        return result.deleted_count\n\n    # # # # # Update # # # # #\n    @classmethod\n    async def update_one(cls, _filter: dict, _update: dict | None = None, /, **kwargs) -> bool:\n        prepare_id_for_query(_filter, is_mongo=True)\n\n        # Step 1: Merge document parameters\n        # Combine the _update dict with keyword arguments into a single document\n        document = cls._merge(_update, kwargs)\n\n        # Step 2: Process and validate document\n        # - Validate data types and structure\n        # - Convert Model instances to their IDs for database storage\n        # - Handle File objects by saving to disk and storing file paths\n        # - Process nested objects and relationships\n        final_document = await cls._process_document(document)\n\n        # Step 3: Create model instance (Validating)\n        # - Retrieve Model instances from database using IDs\n        # - Open File objects from their stored paths\n        # - Build the complete model instance with all relationships\n        await cls._create_model_instance(document=final_document, is_updating=True)\n\n        # Step 4: Create the query\n        update_query = {}\n        for field, value in final_document.items():\n            if field.startswith('$'):\n                update_query[field] = value\n            else:\n                if not '$set' in update_query:\n                    update_query['$set'] = {}\n                update_query['$set'][field] = value\n\n        result = await db.session[cls.__name__].update_one(_filter, update_query)\n        return bool(result.matched_count)\n\n    @classmethod\n    async def update_many(cls, _filter: dict, _update: dict | None = None, /, **kwargs) -> int:\n        prepare_id_for_query(_filter, is_mongo=True)\n\n        # Step 1: Merge document parameters\n        # Combine the _update dict with keyword arguments into a single document\n        document = cls._merge(_update, kwargs)\n\n        # Step 2: Process and validate document\n        # - Validate data types and structure\n        # - Convert Model instances to their IDs for database storage\n        # - Handle File objects by saving to disk and storing file paths\n        # - Process nested objects and relationships\n        final_document = await cls._process_document(document)\n\n        # Step 3: Create model instance (Validating)\n        # - Retrieve Model instances from database using IDs\n        # - Open File objects from their stored paths\n        # - Build the complete model instance with all relationships\n        await cls._create_model_instance(document=final_document, is_updating=True)\n\n        # Step 4: Create the query\n        update_query = {}\n        for field, value in final_document.items():\n            if field.startswith('$'):\n                update_query[field] = value\n            else:\n                if not '$set' in update_query:\n                    update_query['$set'] = {}\n                update_query['$set'][field] = value\n\n        result = await db.session[cls.__name__].update_many(_filter, update_query)\n        return result.modified_count\n"
  },
  {
    "path": "panther/db/queries/pantherdb_queries.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom sys import version_info\n\nfrom pantherdb import Cursor\n\nfrom panther.db.connections import db\nfrom panther.db.cursor import Cursor\nfrom panther.db.queries.base_queries import BaseQuery\nfrom panther.db.utils import prepare_id_for_query\nfrom panther.exceptions import DatabaseError\n\nif version_info >= (3, 11):\n    from typing import Self\nelse:\n    from typing import TypeVar\n\n    Self = TypeVar('Self', bound='BasePantherDBQuery')\n\n\nclass BasePantherDBQuery(BaseQuery):\n    @classmethod\n    def _merge(cls, *args, is_mongo: bool = False) -> dict:\n        return super()._merge(*args, is_mongo=is_mongo)\n\n    # # # # # Find # # # # #\n    @classmethod\n    async def find_one(cls, _filter: dict | None = None, /, **kwargs) -> Self | None:\n        if document := db.session.collection(cls.__name__).find_one(**cls._merge(_filter, kwargs)):\n            return await cls._create_model_instance(document=document)\n        return None\n\n    @classmethod\n    async def find(cls, _filter: dict | None = None, /, **kwargs) -> Cursor:\n        cursor = db.session.collection(cls.__name__).find(**cls._merge(_filter, kwargs))\n        cursor.response_type = cls._create_model_instance\n        cursor.cls = cls\n        return cursor\n\n    @classmethod\n    async def first(cls, _filter: dict | None = None, /, **kwargs) -> Self | None:\n        if document := db.session.collection(cls.__name__).first(**cls._merge(_filter, kwargs)):\n            return await cls._create_model_instance(document=document)\n        return None\n\n    @classmethod\n    async def last(cls, _filter: dict | None = None, /, **kwargs) -> Self | None:\n        if document := db.session.collection(cls.__name__).last(**cls._merge(_filter, kwargs)):\n            return await cls._create_model_instance(document=document)\n        return None\n\n    @classmethod\n    async def aggregate(cls, *args, **kwargs):\n        msg = 'aggregate() does not supported in `PantherDB`.'\n        raise DatabaseError(msg) from None\n\n    # # # # # Count # # # # #\n    @classmethod\n    async def count(cls, _filter: dict | None = None, /, **kwargs) -> int:\n        return db.session.collection(cls.__name__).count(**cls._merge(_filter, kwargs))\n\n    # # # # # Insert # # # # #\n    @classmethod\n    async def insert_one(cls, document: dict) -> Self:\n        insert_one_result = db.session.collection(cls.__name__).insert_one(**document)\n        return insert_one_result['_id']\n\n    @classmethod\n    async def insert_many(cls, documents: Iterable[dict]) -> list[Self]:\n        return [await cls.insert_one(document) for document in documents]\n\n    # # # # # Delete # # # # #\n    @classmethod\n    async def delete_one(cls, _filter: dict | None = None, /, **kwargs) -> bool:\n        return db.session.collection(cls.__name__).delete_one(**cls._merge(_filter, kwargs))\n\n    @classmethod\n    async def delete_many(cls, _filter: dict | None = None, /, **kwargs) -> int:\n        return db.session.collection(cls.__name__).delete_many(**cls._merge(_filter, kwargs))\n\n    # # # # # Update # # # # #\n    @classmethod\n    async def update_one(cls, _filter: dict, _update: dict | None = None, /, **kwargs) -> bool:\n        prepare_id_for_query(_filter)\n\n        # Step 1: Merge document parameters\n        # Combine the _update dict with keyword arguments into a single document\n        document = cls._merge(_update, kwargs)\n\n        # Step 2: Process and validate document\n        # - Validate data types and structure\n        # - Convert Model instances to their IDs for database storage\n        # - Handle File objects by saving to disk and storing file paths\n        # - Process nested objects and relationships\n        final_document = await cls._process_document(document)\n\n        # Step 3: Create model instance (Validating)\n        # - Retrieve Model instances from database using IDs\n        # - Open File objects from their stored paths\n        # - Build the complete model instance with all relationships\n        await cls._create_model_instance(document=final_document, is_updating=True)\n\n        return db.session.collection(cls.__name__).update_one(_filter, **final_document)\n\n    @classmethod\n    async def update_many(cls, _filter: dict, _update: dict | None = None, /, **kwargs) -> int:\n        prepare_id_for_query(_filter)\n\n        # Step 1: Merge document parameters\n        # Combine the _update dict with keyword arguments into a single document\n        document = cls._merge(_update, kwargs)\n\n        # Step 2: Process and validate document\n        # - Validate data types and structure\n        # - Convert Model instances to their IDs for database storage\n        # - Handle File objects by saving to disk and storing file paths\n        # - Process nested objects and relationships\n        final_document = await cls._process_document(document)\n\n        # Step 3: Create model instance (Validating)\n        # - Retrieve Model instances from database using IDs\n        # - Open File objects from their stored paths\n        # - Build the complete model instance with all relationships\n        await cls._create_model_instance(document=final_document, is_updating=True)\n\n        return db.session.collection(cls.__name__).update_many(_filter, **final_document)\n"
  },
  {
    "path": "panther/db/queries/queries.py",
    "content": "import sys\nfrom collections.abc import Iterable, Sequence\n\nfrom pantherdb import Cursor as PantherDBCursor\n\nfrom panther.configs import QueryObservable\nfrom panther.db.cursor import Cursor\nfrom panther.db.queries.base_queries import BaseQuery\nfrom panther.db.utils import check_connection, log_query\nfrom panther.exceptions import NotFoundAPIError\n\n__all__ = ('Query',)\n\nif sys.version_info >= (3, 11):\n    from typing import Self\nelse:\n    from typing import TypeVar\n\n    Self = TypeVar('Self', bound='Query')\n\ntry:\n    from pymongo.errors import DuplicateKeyError as _PyMongoDuplicateKeyError\nexcept ImportError:\n    _PyMongoDuplicateKeyError = None\n\n\ndef _is_duplicate_key_error(error: Exception) -> bool:\n    if _PyMongoDuplicateKeyError and isinstance(error, _PyMongoDuplicateKeyError):\n        return True\n    if error.__class__.__name__ == 'DuplicateKeyError':\n        return True\n    return getattr(error, 'code', None) in {11000, 11001, 12582}\n\n\nclass Query(BaseQuery):\n    def __init_subclass__(cls, **kwargs):\n        QueryObservable.observe(cls)\n\n    @classmethod\n    def _reload_bases(cls, parent):\n        if not issubclass(parent, BaseQuery):\n            msg = f'Invalid Query Class: `{parent.__name__}` should be subclass of `BaseQuery`'\n            raise ValueError(msg)\n\n        if cls.__bases__.count(Query):\n            cls.__bases__ = (*cls.__bases__[: cls.__bases__.index(Query) + 1], parent)\n        else:\n            for kls in cls.__bases__:\n                if kls.__bases__.count(Query):\n                    kls.__bases__ = (*kls.__bases__[: kls.__bases__.index(Query) + 1], parent)\n\n    # # # # # Find # # # # #\n    @classmethod\n    @check_connection\n    @log_query\n    async def find_one(cls, _filter: dict | None = None, /, **kwargs) -> Self | None:\n        \"\"\"\n        Get a single document from the database.\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> await User.find_one(id=1, name='Ali')\n            or\n            >>> await User.find_one({'id': 1, 'name': 'Ali'})\n            or\n            >>> await User.find_one({'id': 1}, name='Ali')\n\n        \"\"\"\n        return await super().find_one(_filter, **kwargs)\n\n    @classmethod\n    @check_connection\n    @log_query\n    async def find(cls, _filter: dict | None = None, /, **kwargs) -> PantherDBCursor | Cursor:\n        \"\"\"\n        Get documents from the database.\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> await User.find(age=18, name='Ali')\n            or\n            >>> await User.find({'age': 18, 'name': 'Ali'})\n            or\n            >>> await User.find({'age': 18}, name='Ali')\n\n        \"\"\"\n        return await super().find(_filter, **kwargs)\n\n    @classmethod\n    @check_connection\n    @log_query\n    async def first(cls, _filter: dict | None = None, /, **kwargs) -> Self | None:\n        \"\"\"\n        Get the first document from the database.\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> await User.first(age=18, name='Ali')\n            or\n            >>> await User.first({'age': 18, 'name': 'Ali'})\n            or\n            >>> await User.first({'age': 18}, name='Ali')\n\n        \"\"\"\n        return await super().first(_filter, **kwargs)\n\n    @classmethod\n    @check_connection\n    @log_query\n    async def last(cls, _filter: dict | None = None, /, **kwargs) -> Self | None:\n        \"\"\"\n        Get the last document from the database.\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> await User.last(age=18, name='Ali')\n            or\n            >>> await User.last({'age': 18, 'name': 'Ali'})\n            or\n            >>> await User.last({'age': 18}, name='Ali')\n\n        \"\"\"\n        return await super().last(_filter, **kwargs)\n\n    @classmethod\n    @check_connection\n    @log_query\n    async def aggregate(cls, pipeline: Sequence[dict]) -> Iterable[dict]:\n        \"\"\"\n        Perform an aggregation using the aggregation framework on this collection.\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> pipeline = [\n            >>>     {'$match': {...}},\n            >>>     {'$unwind': ...},\n            >>>     {'$group': {...}},\n            >>>     {'$project': {...}},\n            >>>     {'$sort': {...}}\n            >>>     ...\n            >>> ]\n\n            >>> await User.aggregate(pipeline)\n\n        \"\"\"\n        return await super().aggregate(pipeline)\n\n    # # # # # Count # # # # #\n    @classmethod\n    @check_connection\n    @log_query\n    async def count(cls, _filter: dict | None = None, /, **kwargs) -> int:\n        \"\"\"\n        Count the number of documents in this collection.\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> await User.count(age=18, name='Ali')\n            or\n            >>> await User.count({'age': 18, 'name': 'Ali'})\n            or\n            >>> await User.count({'age': 18}, name='Ali')\n\n        \"\"\"\n        return await super().count(_filter, **kwargs)\n\n    # # # # # Insert # # # # #\n    @classmethod\n    @check_connection\n    @log_query\n    async def insert_one(cls, _document: dict | None = None, /, **kwargs) -> Self:\n        \"\"\"\n        Insert a single document.\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> await User.insert_one(age=18, name='Ali')\n            or\n            >>> await User.insert_one({'age': 18, 'name': 'Ali'})\n            or\n            >>> await User.insert_one({'age': 18}, name='Ali')\n\n        \"\"\"\n        # Step 1: Merge document parameters\n        # Combine the _document dict with keyword arguments into a single document\n        document = cls._merge(_document, kwargs)\n\n        # Step 2: Process and validate document\n        # - Validate data types and structure\n        # - Convert Model instances to their IDs for database storage\n        # - Handle File objects by saving to disk and storing file paths\n        # - Process nested objects and relationships\n        final_document = await cls._process_document(document)\n\n        # Step 3: Create model instance\n        # - Retrieve Model instances from database using IDs\n        # - Open File objects from their stored paths\n        # - Build the complete model instance with all relationships\n        result = await cls._create_model_instance(document=final_document)\n\n        # Step 4: Insert into database and return\n        # - Insert the processed document into the database\n        # - Assign the generated ID to the model instance\n        # - Return the complete model instance\n        result.id = await super().insert_one(final_document)\n        return result\n\n    @classmethod\n    @check_connection\n    @log_query\n    async def insert_many(cls, documents: Iterable[dict]) -> list[Self]:\n        \"\"\"\n        Insert an iterable of documents.\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> users = [\n            >>>     {'age': 18, 'name': 'Ali'},\n            >>>     {'age': 17, 'name': 'Saba'},\n            >>>     {'age': 16, 'name': 'Amin'}\n            >>> ]\n            >>> await User.insert_many(users)\n\n        \"\"\"\n        return await super().insert_many(documents)\n\n    # # # # # Delete # # # # #\n    @check_connection\n    @log_query\n    async def delete(self) -> None:\n        \"\"\"\n        Delete the document.\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> user = await User.find_one(name='Ali')\n\n            >>> await user.delete()\n\n        \"\"\"\n        await self.delete_one(id=self.id)\n\n    @classmethod\n    @check_connection\n    @log_query\n    async def delete_one(cls, _filter: dict | None = None, /, **kwargs) -> bool:\n        \"\"\"\n        Delete a single document matching the filter.\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> await User.delete_one(age=18, name='Ali')\n            or\n            >>> await User.delete_one({'age': 18, 'name': 'Ali'})\n            or\n            >>> await User.delete_one({'age': 18}, name='Ali')\n\n        \"\"\"\n        return await super().delete_one(_filter, **kwargs)\n\n    @classmethod\n    @check_connection\n    @log_query\n    async def delete_many(cls, _filter: dict | None = None, /, **kwargs) -> int:\n        \"\"\"\n        Delete one or more documents matching the filter.\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> await User.delete_many(age=18, name='Ali')\n            or\n            >>> await User.delete_many({'age': 18, 'name': 'Ali'})\n            or\n            >>> await User.delete_many({'age': 18}, name='Ali')\n\n        \"\"\"\n        return await super().delete_many(_filter, **kwargs)\n\n    # # # # # Update # # # # #\n    @check_connection\n    @log_query\n    async def update(self, _update: dict | None = None, /, **kwargs) -> None:\n        \"\"\"\n        Update the document.\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> user = await User.find_one(age=18, name='Ali')\n\n            >>> await user.update(name='Saba', age=19)\n            or\n            >>> await user.update({'name': 'Saba'}, age=19)\n            or\n            >>> await user.update({'name': 'Saba', 'age': 19})\n\n        \"\"\"\n        await self.update_one({'id': self.id}, _update, **kwargs)\n        await self.reload()\n\n    @classmethod\n    @check_connection\n    @log_query\n    async def update_one(cls, _filter: dict, _update: dict | None = None, /, **kwargs) -> bool:\n        \"\"\"\n        Update a single document matching the filter.\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> await User.update_one({'id': 1}, age=18, name='Ali')\n            or\n            >>> await User.update_one({'id': 1}, {'age': 18, 'name': 'Ali'})\n            or\n            >>> await User.update_one({'id': 1}, {'age': 18}, name='Ali')\n\n        \"\"\"\n        return await super().update_one(_filter, _update, **kwargs)\n\n    @classmethod\n    @check_connection\n    @log_query\n    async def update_many(cls, _filter: dict, _update: dict | None = None, /, **kwargs) -> int:\n        \"\"\"\n        Update one or more documents that match the filter.\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> await User.update_many({'name': 'Saba'}, age=18, name='Ali')\n            or\n            >>> await User.update_many({'name': 'Saba'}, {'age': 18, 'name': 'Ali'})\n            or\n            >>> await User.update_many({'name': 'Saba'}, {'age': 18}, name='Ali')\n\n        \"\"\"\n        return await super().update_many(_filter, _update, **kwargs)\n\n    # # # # # Other # # # # #\n    @classmethod\n    async def all(cls) -> list[Self] | Cursor:\n        \"\"\"\n        Alias of find() without args\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> await User.all()\n\n        \"\"\"\n        return await cls.find()\n\n    @classmethod\n    async def find_one_or_insert(cls, _filter: dict | None = None, /, **kwargs) -> tuple[Self, bool]:\n        \"\"\"\n        Get a single document from the database.\n        or\n        Insert a single document.\n        This method requires a unique index/constraint on queried fields to be correct under concurrency.\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> await User.find_one_or_insert(age=18, name='Ali')\n            or\n            >>> await User.find_one_or_insert({'age': 18, 'name': 'Ali'})\n            or\n            >>> await User.find_one_or_insert({'age': 18}, name='Ali')\n\n        \"\"\"\n        if obj := await cls.find_one(_filter, **kwargs):\n            return obj, False\n        try:\n            return await cls.insert_one(_filter, **kwargs), True\n        except Exception as e:\n            if not _is_duplicate_key_error(e):\n                raise\n            if obj := await cls.find_one(_filter, **kwargs):\n                return obj, False\n            raise\n\n    @classmethod\n    async def find_one_or_raise(cls, _filter: dict | None = None, /, **kwargs) -> Self:\n        \"\"\"\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> await User.find_one_or_raise(age=18, name='Ali')\n            or\n            >>> await User.find_one_or_raise({'age': 18, 'name': 'Ali'})\n            or\n            >>> await User.find_one_or_raise({'age': 18}, name='Ali')\n\n        \"\"\"\n        if obj := await cls.find_one(_filter, **kwargs):\n            return obj\n\n        raise NotFoundAPIError(detail=f'{cls.__name__} Does Not Exist')\n\n    @classmethod\n    async def exists(cls, _filter: dict | None = None, /, **kwargs) -> bool:\n        \"\"\"\n        Check if document exists in collection or not\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            >>> await User.exists(age=18, name='Ali')\n            or\n            >>> await User.exists({'age': 18, 'name': 'Ali'})\n            or\n            >>> await User.exists({'age': 18}, name='Ali')\n\n        \"\"\"\n        return await cls.count(_filter, **kwargs) > 0\n\n    async def save(self) -> None:\n        \"\"\"\n        Save the document\n            If it has `id` --> Update It\n            else --> Insert It\n\n        Example:\n        -------\n            >>> from app.models import User\n\n            # Update\n            >>> user = await User.find_one(name='Ali')\n            >>> user.name = 'Saba'\n            >>> await user.save()\n            or\n            # Insert\n            >>> user = User(name='Ali')\n            >>> await user.save()\n\n        \"\"\"\n        document = self.model_dump(exclude={'id'})\n        if self.id:\n            await self.update(document)\n        else:\n            self.id = (await self.insert_one(document)).id\n\n    async def reload(self):\n        updated_instance = await self.find_one(id=self.id)\n        for field_name in self.__class__.model_fields:\n            setattr(self, field_name, getattr(updated_instance, field_name))\n"
  },
  {
    "path": "panther/db/utils.py",
    "content": "import logging\nfrom time import perf_counter\n\nfrom panther.configs import config\n\ntry:\n    # Only required if user wants to use mongodb\n    import bson\nexcept ImportError:\n    pass\n\nlogger = logging.getLogger('query')\n\n\ndef log_query(func):\n    async def log(*args, **kwargs):\n        if config.LOG_QUERIES is False:\n            return await func(*args, **kwargs)\n        start = perf_counter()\n        response = await func(*args, **kwargs)\n        end = perf_counter()\n        class_name = getattr(args[0], '__name__', args[0].__class__.__name__)\n        logger.info(f'[Query] {class_name}.{func.__name__}() takes {(end - start) * 1_000:.3} ms')\n        return response\n\n    return log\n\n\ndef check_connection(func):\n    async def wrapper(*args, **kwargs):\n        if config.QUERY_ENGINE is None:\n            msg = (\n                \"You don't have active database connection, Check your DATABASE block in configs\"\n                '\\nMore Info: https://PantherPy.GitHub.io/database/'\n            )\n            raise NotImplementedError(msg)\n        return await func(*args, **kwargs)\n\n    return wrapper\n\n\ndef prepare_id_for_query(*args, is_mongo: bool = False):\n    for d in args:\n        if d is None:\n            continue\n        if 'id' in d:\n            d['_id'] = d.pop('id')\n\n        if '_id' in d:\n            _converter = _convert_to_object_id if is_mongo else str\n            d['_id'] = _converter(d['_id'])\n\n\ndef _convert_to_object_id(_id):\n    if _id is None:\n        return None\n    if isinstance(_id, bson.ObjectId):\n        return _id\n    try:\n        return bson.ObjectId(_id)\n    except bson.objectid.InvalidId:\n        logger.warning(f'id={_id} is not a valid bson.ObjectId')\n        return None\n"
  },
  {
    "path": "panther/events.py",
    "content": "import asyncio\nimport logging\n\nfrom panther._utils import is_function_async\nfrom panther.background_tasks import get_application_event_loop\nfrom panther.utils import Singleton\n\nlogger = logging.getLogger('panther')\n\n\nclass Event(Singleton):\n    _startups = []\n    _shutdowns = []\n\n    @classmethod\n    def startup(cls, func):\n        cls._startups.append(func)\n\n        def wrapper():\n            return func()\n\n        return wrapper\n\n    @classmethod\n    def shutdown(cls, func):\n        cls._shutdowns.append(func)\n\n        def wrapper():\n            return func()\n\n        return wrapper\n\n    @classmethod\n    async def run_startups(cls):\n        for func in cls._startups:\n            try:\n                if is_function_async(func):\n                    await func()\n                else:\n                    func()\n            except Exception as e:\n                logger.error(f'{func.__name__}() startup event got error: {e}')\n\n    @classmethod\n    def run_shutdowns(cls):\n        for func in cls._shutdowns:\n            if is_function_async(func):\n                try:\n                    app_loop = get_application_event_loop()\n                    if app_loop and app_loop.is_running() and not app_loop.is_closed():\n                        asyncio.run_coroutine_threadsafe(func(), app_loop).result()\n                    else:\n                        asyncio.run(func())\n                except ModuleNotFoundError:\n                    # Error: import of asyncio halted; None in sys.modules\n                    #   And as I figured it out, it only happens when we are running with\n                    #   gunicorn and Uvicorn workers (-k uvicorn.workers.UvicornWorker)\n                    pass\n            else:\n                func()\n\n    @classmethod\n    def clear(cls):\n        \"\"\"Clear all stored events (useful for testing)\"\"\"\n        cls._startups.clear()\n        cls._shutdowns.clear()\n"
  },
  {
    "path": "panther/exceptions.py",
    "content": "from panther import status\n\n\nclass PantherError(Exception):\n    pass\n\n\nclass DatabaseError(Exception):\n    pass\n\n\nclass BaseError(Exception):\n    detail: str | dict | list = 'Internal Server Error'\n    status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR\n\n    def __init__(self, detail: str | dict | list = None, status_code: int = None, headers: dict = None):\n        self.detail = detail or self.detail\n        self.status_code = status_code or self.status_code\n        self.headers = headers\n\n\nclass APIError(BaseError):\n    detail: str | dict | list = 'Internal Server Error'\n    status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR\n\n\nclass WebsocketError(BaseError):\n    detail: str | dict | list = 'Internal Error'\n    status_code: int = status.WS_1011_INTERNAL_ERROR\n\n\nclass RedirectAPIError(APIError):\n    def __init__(self, url: str, status_code: int = status.HTTP_302_FOUND):\n        super().__init__(headers={'Location': url}, status_code=status_code)\n\n\nclass BadRequestAPIError(APIError):\n    detail = 'Bad Request'\n    status_code = status.HTTP_400_BAD_REQUEST\n\n\nclass AuthenticationAPIError(APIError):\n    detail = 'Authentication Error'\n    status_code = status.HTTP_401_UNAUTHORIZED\n\n\nclass AuthorizationAPIError(APIError):\n    detail = 'Permission Denied'\n    status_code = status.HTTP_403_FORBIDDEN\n\n\nclass NotFoundAPIError(APIError):\n    detail = 'Not Found'\n    status_code = status.HTTP_404_NOT_FOUND\n\n\nclass MethodNotAllowedAPIError(APIError):\n    detail = 'Method Not Allowed'\n    status_code = status.HTTP_405_METHOD_NOT_ALLOWED\n\n\nclass UnprocessableEntityError(APIError):\n    detail = 'Unprocessable Entity Error'\n    status_code = status.HTTP_422_UNPROCESSABLE_ENTITY\n\n\nclass UpgradeRequiredError(APIError):\n    detail = 'This service requires use of the WebSocket protocol.'\n    status_code = status.HTTP_426_UPGRADE_REQUIRED\n\n\nclass ThrottlingAPIError(APIError):\n    detail = 'Too Many Request'\n    status_code = status.HTTP_429_TOO_MANY_REQUESTS\n\n\nclass InvalidPathVariableAPIError(APIError):\n    def __init__(self, value: str, variable_type: type):\n        detail = f'Path variable `{value}` is not `{variable_type.__name__}`'\n        super().__init__(detail=detail, status_code=status.HTTP_400_BAD_REQUEST)\n"
  },
  {
    "path": "panther/file_handler.py",
    "content": "from functools import cached_property\nfrom io import BufferedReader, BytesIO\nfrom pathlib import Path\n\nfrom pydantic import BaseModel, field_validator\n\nfrom panther import status\nfrom panther.exceptions import APIError\nfrom panther.utils import timezone_now\n\n\nclass File(BaseModel):\n    file_name: str\n    content_type: str\n    file: bytes | None = None\n    _file_path: Path | None = None\n    _buffer: BytesIO | BufferedReader | None = None\n\n    def __init__(self, **data):\n        super().__init__(**data)\n        if self.file:\n            self._buffer = BytesIO(self.file)\n        elif 'file_name' in data:\n            self._file_path = Path(data['file_name'])\n        self._saved_path: str | None = None\n\n    def __enter__(self):\n        if not self._buffer:\n            # Open file lazily in binary read mode\n            self._buffer = open(self._file_path, 'rb')\n        return self\n\n    def __exit__(self, exc_type, exc_value, traceback):\n        if self._buffer:\n            self._buffer.close()\n            self._buffer = None\n\n    def read(self, size: int = -1) -> bytes:\n        self._ensure_buffer()\n        return self._buffer.read(size)\n\n    def seek(self, offset: int, whence: int = 0):\n        self._ensure_buffer()\n        return self._buffer.seek(offset, whence)\n\n    def tell(self) -> int:\n        self._ensure_buffer()\n        return self._buffer.tell()\n\n    def write(self, data: bytes):\n        if isinstance(self._buffer, BytesIO):\n            self._buffer.seek(0, 2)\n            self._buffer.write(data)\n            self.file = self._buffer.getvalue()  # sync updated bytes\n        else:\n            raise IOError('Write is only supported for in-memory files.')\n\n    def save(self, path: str | None = None, overwrite: bool = False) -> str:\n        # If already saved, return the same path\n        if self._saved_path is not None:\n            return self._saved_path\n\n        self._ensure_buffer()\n\n        # Handle directory paths (ending with slash)\n        if path and str(path).endswith('/'):\n            # Treat as directory, use original file name\n            base_path = Path(path) / self.file_name\n        else:\n            base_path = Path(path or self.file_name)\n\n        if not overwrite:\n            file_path = base_path\n            if file_path.exists():\n                # Format: file_YYYYMMDD_HHMMSS[_N].ext\n                timestamp = timezone_now().strftime('%Y%m%d_%H%M%S')\n                file_path = base_path.with_name(f'{base_path.stem}_{timestamp}{base_path.suffix}')\n\n                # Ensure uniqueness if file with same timestamp exists\n                counter = 1\n                while file_path.exists():\n                    file_path = base_path.with_name(f'{base_path.stem}_{timestamp}_{counter}{base_path.suffix}')\n                    counter += 1\n        else:\n            file_path = base_path\n\n        # Ensure directory exists\n        file_path.parent.mkdir(parents=True, exist_ok=True)\n\n        # Write file\n        with open(file_path, 'wb') as f:\n            f.write(self._buffer.read())\n            self._buffer.seek(0)\n\n        # Store the saved path for idempotency\n        self._saved_path = str(file_path)\n        return self._saved_path\n\n    @cached_property\n    def size(self) -> int:\n        if self.file is not None:\n            return len(self.file)\n        if self._file_path:\n            return self._file_path.stat().st_size\n        return 0\n\n    def _ensure_buffer(self):\n        if not self._buffer:\n            if self._file_path:\n                self._buffer = open(self._file_path, 'rb')\n            elif self.file is not None:\n                self._buffer = BytesIO(self.file)\n            else:\n                raise ValueError('No file source to read from.')\n\n    def __repr__(self) -> str:\n        return f'{self.__repr_name__()}(file_name={self.file_name}, content_type={self.content_type})'\n\n    __str__ = __repr__\n\n\nclass Image(File):\n    @field_validator('content_type')\n    @classmethod\n    def validate_content_type(cls, content_type: str) -> str:\n        if not content_type.startswith('image/'):\n            msg = f\"{content_type} is not a valid image 'content_type'\"\n            raise APIError(detail=msg, status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE)\n        return content_type\n"
  },
  {
    "path": "panther/generics.py",
    "content": "import contextlib\nimport logging\nimport re\nfrom abc import abstractmethod\n\nfrom pantherdb import Cursor as PantherDBCursor\n\nfrom panther import status\nfrom panther.app import GenericAPI\nfrom panther.configs import config\nfrom panther.db import Model\nfrom panther.db.connections import MongoDBConnection\nfrom panther.db.cursor import Cursor\nfrom panther.db.models import ID\nfrom panther.exceptions import APIError\nfrom panther.pagination import Pagination\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.serializer import ModelSerializer\n\nwith contextlib.suppress(ImportError):\n    # Only required if user wants to use mongodb\n    import bson\n\nlogger = logging.getLogger('panther')\n\n\nclass RetrieveAPI(GenericAPI):\n    @abstractmethod\n    async def get_instance(self, request: Request, **kwargs) -> Model:\n        \"\"\"\n        Should return an instance of Model, e.g. `await User.find_one()`\n        \"\"\"\n        logger.error(f'`get_instance()` method is not implemented in {self.__class__} .')\n        raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)\n\n    async def get(self, request: Request, **kwargs):\n        instance = await self.get_instance(request=request, **kwargs)\n        return Response(data=instance, status_code=status.HTTP_200_OK)\n\n\nclass ListAPI(GenericAPI):\n    sort_fields: list[str] = []\n    search_fields: list[str] = []\n    filter_fields: list[str] = []\n    pagination: type[Pagination] | None = None\n\n    @abstractmethod\n    async def get_query(self, request: Request, **kwargs):\n        \"\"\"\n        Should return a Cursor, e.g. `await User.find()`\n        \"\"\"\n        logger.error(f'`get_query()` method is not implemented in {self.__class__} .')\n        raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)\n\n    async def get(self, request: Request, **kwargs):\n        cursor, pagination = await self.prepare_cursor(request=request, **kwargs)\n        return Response(data=cursor, pagination=pagination, status_code=status.HTTP_200_OK)\n\n    async def prepare_cursor(self, request: Request, **kwargs) -> tuple[Cursor | PantherDBCursor, Pagination | None]:\n        cursor = await self.get_query(request=request, **kwargs)\n        if not isinstance(cursor, (Cursor, PantherDBCursor)):\n            logger.error(f'`{self.__class__.__name__}.get_query()` should return a Cursor, e.g. `await Model.find()`')\n            raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)\n\n        query = {}\n        query |= self.process_filters(query_params=request.query_params, cursor=cursor)\n        query |= self.process_search(query_params=request.query_params)\n\n        if query:\n            cursor = await cursor.cls.find(cursor.filter | query)\n\n        if sort := self.process_sort(query_params=request.query_params):\n            cursor = cursor.sort(sort)\n\n        if pagination := self.process_pagination(query_params=request.query_params, cursor=cursor):\n            cursor = pagination.paginate()\n\n        return cursor, pagination\n\n    def process_filters(self, query_params: dict, cursor: Cursor | PantherDBCursor) -> dict:\n        _filter = {}\n        for field in self.filter_fields:\n            if field in query_params:\n                _filter[field] = query_params[field]\n                if isinstance(config.DATABASE, MongoDBConnection) and cursor.cls.model_fields[field].annotation == ID:\n                    _filter[field] = bson.ObjectId(_filter[field])\n        return _filter\n\n    def process_search(self, query_params: dict) -> dict:\n        search_param = query_params.get('search')\n        if not self.search_fields or not search_param:\n            return {}\n        if isinstance(config.DATABASE, MongoDBConnection):\n            escaped_search_param = re.escape(search_param)\n            if search := [{field: {'$regex': escaped_search_param}} for field in self.search_fields]:\n                return {'$or': search}\n        return {field: search_param for field in self.search_fields}\n\n    def process_sort(self, query_params: dict) -> list:\n        sort_param = query_params.get('sort')\n        if not self.sort_fields or not sort_param:\n            return []\n        return [\n            (field, -1 if param.startswith('-') else 1)\n            for param in sort_param.split(',')\n            for field in self.sort_fields\n            if field == param.removeprefix('-')\n        ]\n\n    def process_pagination(self, query_params: dict, cursor: Cursor | PantherDBCursor) -> Pagination | None:\n        if self.pagination:\n            return self.pagination(query_params=query_params, cursor=cursor)\n\n\nclass CreateAPI(GenericAPI):\n    async def post(self, request: Request, **kwargs):\n        instance = await request.validated_data.model.insert_one(request.validated_data.model_dump())\n        return Response(data=instance, status_code=status.HTTP_201_CREATED)\n\n\nclass UpdateAPI(GenericAPI):\n    @abstractmethod\n    async def get_instance(self, request: Request, **kwargs) -> Model:\n        \"\"\"\n        Should return an instance of Model, e.g. `await User.find_one()`\n        \"\"\"\n        logger.error(f'`get_instance()` method is not implemented in {self.__class__} .')\n        raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)\n\n    async def put(self, request: Request, **kwargs):\n        instance = await self.get_instance(request=request, **kwargs)\n        await instance.update(request.validated_data.model_dump())\n        return Response(data=instance, status_code=status.HTTP_200_OK)\n\n    async def patch(self, request: Request, **kwargs):\n        instance = await self.get_instance(request=request, **kwargs)\n        await instance.update(request.validated_data.model_dump(exclude_none=True))\n        return Response(data=instance, status_code=status.HTTP_200_OK)\n\n\nclass DeleteAPI(GenericAPI):\n    @abstractmethod\n    async def get_instance(self, request: Request, **kwargs) -> Model:\n        \"\"\"\n        Should return an instance of Model, e.g. `await User.find_one()`\n        \"\"\"\n        logger.error(f'`get_instance()` method is not implemented in {self.__class__} .')\n        raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)\n\n    async def pre_delete(self, instance, request: Request, **kwargs):\n        \"\"\"Hook for logic before deletion.\"\"\"\n        pass\n\n    async def post_delete(self, instance, request: Request, **kwargs):\n        \"\"\"Hook for logic after deletion.\"\"\"\n        pass\n\n    async def delete(self, request: Request, **kwargs):\n        instance = await self.get_instance(request=request, **kwargs)\n        await self.pre_delete(instance, request=request, **kwargs)\n        await instance.delete()\n        await self.post_delete(instance, request=request, **kwargs)\n        return Response(status_code=status.HTTP_204_NO_CONTENT)\n"
  },
  {
    "path": "panther/logging.py",
    "content": "import logging\nfrom pathlib import Path\n\nfrom panther.configs import config\n\nLOGS_DIR = config.BASE_DIR / 'logs'\n\n\nclass FileHandler(logging.FileHandler):\n    def __init__(self, filename, mode='a', encoding=None, delay=False, errors=None):\n        Path(LOGS_DIR).mkdir(exist_ok=True)\n        logging.FileHandler.__init__(self, filename, mode=mode, encoding=encoding, delay=delay, errors=errors)\n\n\nLOGGING = {\n    'version': 1,\n    'disable_existing_loggers': False,\n    'formatters': {\n        'default': {\n            '()': 'uvicorn.logging.DefaultFormatter',\n            'fmt': '%(levelprefix)s | %(asctime)s | %(message)s',\n            'datefmt': '%Y-%m-%d %H:%M:%S',\n        },\n        'panther_file_formatter': {\n            '()': 'uvicorn.logging.DefaultFormatter',\n            'fmt': '%(asctime)s | %(message)s',\n            'datefmt': '%Y-%m-%d %H:%M:%S',\n        },\n    },\n    'handlers': {\n        'panther_file': {\n            'formatter': 'panther_file_formatter',\n            'filename': LOGS_DIR / 'main.log',\n            'class': 'panther.logging.FileHandler',\n            'delay': True,\n        },\n        'monitoring_file': {\n            'formatter': 'panther_file_formatter',\n            'filename': LOGS_DIR / 'monitoring.log',\n            'class': 'panther.logging.FileHandler',\n            'delay': True,\n        },\n        'query_file': {\n            'formatter': 'panther_file_formatter',\n            'filename': LOGS_DIR / 'query.log',\n            'class': 'panther.logging.FileHandler',\n            'delay': True,\n        },\n        'default': {\n            'formatter': 'default',\n            'class': 'logging.StreamHandler',\n            'stream': 'ext://sys.stderr',\n        },\n    },\n    'loggers': {\n        'panther': {\n            'handlers': ['default', 'panther_file'],\n            'level': 'DEBUG',\n        },\n        'monitoring': {\n            'handlers': ['monitoring_file'],\n            'level': 'DEBUG',\n        },\n        'query': {\n            'handlers': ['default', 'query_file'],\n            'level': 'DEBUG',\n        },\n    },\n}\n"
  },
  {
    "path": "panther/main.py",
    "content": "import asyncio\nimport logging\nimport sys\nfrom collections.abc import Callable\nfrom logging.config import dictConfig\nfrom pathlib import Path\n\nimport panther.logging\nfrom panther import status\nfrom panther._load_configs import *\nfrom panther._utils import (\n    ENDPOINT_CLASS_BASED_API,\n    ENDPOINT_FUNCTION_BASED_API,\n    ENDPOINT_WEBSOCKET,\n    reformat_code,\n    traceback_message,\n)\nfrom panther.background_tasks import (\n    register_application_event_loop,\n)\nfrom panther.base_websocket import Websocket\nfrom panther.cli.utils import print_info\nfrom panther.configs import config\nfrom panther.events import Event\nfrom panther.exceptions import APIError, BaseError, NotFoundAPIError, PantherError, UpgradeRequiredError\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.routings import find_endpoint\nfrom panther.websocket import GenericWebsocket\n\ndictConfig(panther.logging.LOGGING)\nlogger = logging.getLogger('panther')\n\n\nclass Panther:\n    def __init__(self, name: str, configs: str | None = None, urls: dict | None = None):\n        \"\"\"\n        Initialize a Panther application instance.\n\n        Args:\n            name: Typically set to `__name__`; used to determine the current directory of the application.\n            configs: The name of the module containing your configuration.\n                If the configuration is defined in the current file, you can also set this to `__name__`.\n            urls: A dictionary containing your URL routing.\n                If not provided, Panther will attempt to load `URLs` from the configs module.\n\n        \"\"\"\n        self._configs_module_name = configs\n        self._urls = urls\n\n        config.BASE_DIR = Path(name).resolve().parent\n\n        try:\n            self.load_configs()\n            if config.AUTO_REFORMAT:\n                reformat_code(base_dir=config.BASE_DIR)\n        except Exception as e:\n            logger.error(e.args[0] if isinstance(e, PantherError) else traceback_message(exception=e))\n            sys.exit()\n\n        # Print Info\n        print_info(config)\n\n    def load_configs(self) -> None:\n        # Check & Read The Configs File\n        self._configs_module = load_configs_module(self._configs_module_name)\n\n        load_redis(self._configs_module)\n        load_timezone(self._configs_module)\n        load_database(self._configs_module)\n        load_secret_key(self._configs_module)\n        load_throttling(self._configs_module)\n        load_user_model(self._configs_module)\n        load_log_queries(self._configs_module)\n        load_templates_dir(self._configs_module)\n        load_middlewares(self._configs_module)\n        load_auto_reformat(self._configs_module)\n        load_background_tasks(self._configs_module)\n        load_other_configs(self._configs_module)\n        load_urls(self._configs_module, urls=self._urls)\n        load_authentication_class(self._configs_module)\n        load_websocket_connections()\n\n        check_endpoints_inheritance()\n\n    async def __call__(self, scope: dict, receive: Callable, send: Callable) -> None:\n        register_application_event_loop(asyncio.get_running_loop())\n\n        if scope['type'] == 'http':\n            await self.handle_http(scope=scope, receive=receive, send=send)\n        elif scope['type'] == 'websocket':\n            await self.handle_ws(scope=scope, receive=receive, send=send)\n        elif scope['type'] == 'lifespan':\n            message = await receive()\n            if message['type'] == 'lifespan.startup':\n                if config.HAS_WS:\n                    await config.WEBSOCKET_CONNECTIONS.start()\n                await Event.run_startups()\n            elif message['type'] == 'lifespan.shutdown':\n                pass\n            return\n\n    @staticmethod\n    async def handle_ws_endpoint(connection: Websocket):\n        # Find Endpoint\n        endpoint, found_path = find_endpoint(path=connection.path)\n        if endpoint is None:\n            await connection.close()\n            return connection\n\n        # Check Endpoint Type\n        if endpoint._endpoint_type is not ENDPOINT_WEBSOCKET:\n            logger.warning(f'{endpoint.__name__}() class is not a subclass of `GenericWebsocket`.')\n            await connection.close()\n            return connection\n\n        # Create The Connection\n        final_connection = endpoint(parent=connection)\n        del connection\n\n        # Collect Path Variables\n        final_connection.collect_path_variables(found_path=found_path)\n\n        return await config.WEBSOCKET_CONNECTIONS.listen(connection=final_connection)\n\n    @classmethod\n    async def handle_ws(cls, scope: dict, receive: Callable, send: Callable) -> None:\n        # Create Temp Connection\n        connection = Websocket(scope=scope, receive=receive, send=send)\n\n        # Create Middlewares chain\n        chained_func = cls.handle_ws_endpoint\n        for middleware in config.WS_MIDDLEWARES:\n            chained_func = middleware(dispatch=chained_func)\n\n        # Call Middlewares & Endpoint\n        try:\n            connection = await chained_func(connection=connection)\n        except BaseError as e:\n            connection.log(e.detail)\n            await connection.close()\n        except Exception as e:\n            logger.error(traceback_message(exception=e))\n            await connection.close()\n\n    @staticmethod\n    async def handle_http_endpoint(request: Request) -> Response:\n        # Find Endpoint\n        endpoint, found_path = find_endpoint(path=request.path)\n        if endpoint is None:\n            raise NotFoundAPIError\n\n        # Collect Path Variables\n        request.collect_path_variables(found_path=found_path)\n\n        if endpoint._endpoint_type is ENDPOINT_FUNCTION_BASED_API:\n            return await endpoint(request=request)\n        if endpoint._endpoint_type is ENDPOINT_CLASS_BASED_API:\n            return await endpoint().call_method(request=request)\n\n        # ENDPOINT_WEBSOCKET\n        raise UpgradeRequiredError\n\n    @classmethod\n    async def handle_http(cls, scope: dict, receive: Callable, send: Callable) -> None:\n        # Create `Request` and its body\n        request = Request(scope=scope, receive=receive, send=send)\n        await request.read_body()\n\n        # Create Middlewares chain\n        chained_func = cls.handle_http_endpoint\n        for middleware in config.HTTP_MIDDLEWARES:\n            chained_func = middleware(dispatch=chained_func)\n\n        # Call Middlewares & Endpoint\n        try:\n            response = await chained_func(request=request)\n            if response is None:\n                logger.error('You forgot to return `response` on the `Middlewares.__call__()`')\n                response = Response(\n                    data={'detail': 'Internal Server Error'},\n                    status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,\n                )\n        except APIError as e:\n            response = Response(\n                data=e.detail if isinstance(e.detail, dict) else {'detail': e.detail},\n                headers=e.headers,\n                status_code=e.status_code,\n            )\n        except Exception as e:  # Handle Unknown Exceptions\n            logger.error(traceback_message(exception=e))\n            response = Response(\n                data={'detail': 'Internal Server Error'},\n                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,\n            )\n\n        # Return Response\n        await response.send(send=send, receive=receive)\n\n    def __del__(self):\n        Event.run_shutdowns()\n"
  },
  {
    "path": "panther/middlewares/__init__.py",
    "content": "from panther.middlewares.base import HTTPMiddleware, WebsocketMiddleware  # noqa: F401\nfrom panther.middlewares.cors import CORSMiddleware  # noqa: F401\nfrom panther.middlewares.monitoring import MonitoringMiddleware, WebsocketMonitoringMiddleware  # noqa: F401\n"
  },
  {
    "path": "panther/middlewares/base.py",
    "content": "import typing\n\nfrom panther.base_websocket import Websocket\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.websocket import GenericWebsocket\n\n\nclass HTTPMiddleware:\n    \"\"\"Used only in http requests\"\"\"\n\n    def __init__(self, dispatch: typing.Callable):\n        self.dispatch = dispatch\n\n    async def __call__(self, request: Request) -> Response:\n        return await self.dispatch(request=request)\n\n\nclass WebsocketMiddleware:\n    \"\"\"Used only in ws requests\"\"\"\n\n    def __init__(self, dispatch: typing.Callable):\n        self.dispatch = dispatch\n\n    async def __call__(self, connection: Websocket) -> GenericWebsocket:\n        return await self.dispatch(connection=connection)\n"
  },
  {
    "path": "panther/middlewares/cors.py",
    "content": "from panther.configs import config\nfrom panther.middlewares import HTTPMiddleware\nfrom panther.request import Request\nfrom panther.response import Response\n\n\nclass CORSMiddleware(HTTPMiddleware):\n    \"\"\"\n    Middleware to handle Cross-Origin Resource Sharing (CORS) for Panther applications.\n\n    This middleware automatically adds the appropriate CORS headers to all HTTP responses\n    based on configuration variables defined in your Panther config file (e.g., core/configs.py).\n    It also handles preflight (OPTIONS) requests.\n\n    Configuration attributes (set these in your config):\n    ---------------------------------------------------\n    ALLOW_ORIGINS: list[str]\n        List of allowed origins. Use [\"*\"] to allow all origins. Default: [\"*\"]\n    ALLOW_METHODS: list[str]\n        List of allowed HTTP methods. Default: [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"OPTIONS\"]\n    ALLOW_HEADERS: list[str]\n        List of allowed request headers. Use [\"*\"] to allow all headers. Default: [\"*\"]\n    ALLOW_CREDENTIALS: bool\n        Whether to allow credentials (cookies, authorization headers, etc.). Default: False\n    EXPOSE_HEADERS: list[str]\n        List of headers that can be exposed to the browser. Default: []\n    CORS_MAX_AGE: int\n        Number of seconds browsers are allowed to cache preflight responses. Default: 600\n\n    Usage:\n    ------\n    1. Set the above config variables in your config file as needed.\n    2. Add 'panther.middlewares.cors.CORSMiddleware' to your MIDDLEWARES list.\n    \"\"\"\n\n    async def __call__(self, request: Request) -> Response:\n        # Fetch CORS settings from config, with defaults\n        allow_origins = config.ALLOW_ORIGINS or ['*']\n        allow_methods = config.ALLOW_METHODS or ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']\n        allow_headers = config.ALLOW_HEADERS or ['*']\n        allow_credentials = config.ALLOW_CREDENTIALS or False\n        expose_headers = config.EXPOSE_HEADERS or []\n        max_age = config.CORS_MAX_AGE or 600\n\n        # Handle preflight (OPTIONS) requests\n        if request.method == 'OPTIONS':\n            response = Response(status_code=204)\n        else:\n            response = await self.dispatch(request=request)\n\n        origin = request.headers['origin'] or '*'\n        if '*' in allow_origins:\n            allow_origin = '*'\n        elif origin in allow_origins:\n            allow_origin = origin\n        else:\n            allow_origin = allow_origins[0] if allow_origins else '*'\n\n        response.headers['Access-Control-Allow-Origin'] = allow_origin\n        response.headers['Access-Control-Allow-Methods'] = ', '.join(allow_methods)\n        response.headers['Access-Control-Allow-Headers'] = ', '.join(allow_headers)\n        response.headers['Access-Control-Max-Age'] = str(max_age)\n        if allow_credentials:\n            response.headers['Access-Control-Allow-Credentials'] = 'true'\n        if expose_headers:\n            response.headers['Access-Control-Expose-Headers'] = ', '.join(expose_headers)\n        return response\n"
  },
  {
    "path": "panther/middlewares/monitoring.py",
    "content": "import logging\nfrom time import perf_counter\n\nfrom panther.base_websocket import Websocket\nfrom panther.middlewares import HTTPMiddleware, WebsocketMiddleware\nfrom panther.request import Request\n\nlogger = logging.getLogger('monitoring')\n\n\nclass MonitoringMiddleware(HTTPMiddleware):\n    \"\"\"\n    Create Log Message Like Below:\n    datetime | method | path | ip:port | response_time(seconds) | status\n    \"\"\"\n\n    async def __call__(self, request: Request):\n        start_time = perf_counter()\n        method = request.scope['method']\n\n        response = await self.dispatch(request=request)\n\n        response_time = perf_counter() - start_time  # Seconds\n        logger.info(f'{method} | {request.path} | {request.client} | {response_time} | {response.status_code}')\n        return response\n\n\nclass WebsocketMonitoringMiddleware(WebsocketMiddleware):\n    \"\"\"\n    Create Log Message Like Below:\n    datetime | WS | path | ip:port | connection_time(seconds) | status\n    \"\"\"\n\n    ConnectedConnectionTime = ' - '\n\n    async def __call__(self, connection: Websocket):\n        start_time = perf_counter()\n\n        logger.info(f'WS | {connection.path} | {connection.client} |{self.ConnectedConnectionTime}| {connection.state}')\n        connection = await self.dispatch(connection=connection)\n\n        connection_time = perf_counter() - start_time  # Seconds\n        logger.info(f'WS | {connection.path} | {connection.client} | {connection_time} | {connection.state}')\n        return connection\n"
  },
  {
    "path": "panther/openapi/__init__.py",
    "content": "from panther.openapi.schemas import OutputSchema\n"
  },
  {
    "path": "panther/openapi/schemas.py",
    "content": "import pydantic\n\nfrom panther.serializer import ModelSerializer\n\n\nclass OutputSchema:\n    \"\"\"\n    Configuration class for defining API endpoint output schemas.\n\n    This class allows you to specify the response model, status code, and other\n    metadata for API endpoints to generate proper OpenAPI documentation.\n\n    Attributes:\n        model: The Pydantic model or ModelSerializer class for the response\n        status_code: HTTP status code for the response\n        exclude_in_docs: Whether to exclude this endpoint from OpenAPI docs (defaults to False)\n        tags: List of tags for grouping endpoints in documentation (defaults to Function Name/ Class Name)\n        deprecated: Whether this endpoint is marked as deprecated (defaults to False)\n    \"\"\"\n\n    def __init__(\n        self,\n        model: type[ModelSerializer] | type[pydantic.BaseModel] | None = None,\n        status_code: int | None = None,\n        exclude_in_docs: bool = False,\n        tags: str | None = None,\n        deprecated: bool = False,\n    ):\n        self.model = model\n        self.status_code = status_code\n        self.exclude_in_docs = exclude_in_docs\n        self.tags = tags\n        self.deprecated = deprecated\n"
  },
  {
    "path": "panther/openapi/templates/rapidoc.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>RapiDoc Inline JSON</title>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes\">\n    <script type=\"module\" src=\"https://unpkg.com/rapidoc/dist/rapidoc-min.js\"></script>\n  </head>\n  <body>\n    <rapi-doc id=\"thedoc\"\n      render-style = \"read\"\n      allow-try=\"false\"\n      allow-authentication = \"false\"\n    > </rapi-doc>\n    <script>\n      document.addEventListener('readystatechange', (event) => {\n        let docEl = document.getElementById(\"thedoc\");\n        docEl.loadSpec({{ openapi | tojson }});\n      })\n    </script>\n  </body>\n</html>"
  },
  {
    "path": "panther/openapi/templates/redoc.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>ReDoc API Reference</title>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        margin: 0;\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div id=\"redoc-container\"></div>\n    <script src=\"https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js\"></script>\n    <script>\n      Redoc.init({{ openapi | tojson }}, {}, document.getElementById(\"redoc-container\"));\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "panther/openapi/templates/scalar.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Scalar API Reference</title>\n    <meta charset=\"utf-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, initial-scale=1\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script src=\"https://cdn.jsdelivr.net/npm/@scalar/api-reference\"></script>\n    <script>\n      Scalar.createApiReference('#app', {\n        content: {{ openapi | tojson }}\n      })\n    </script>\n  </body>\n</html>"
  },
  {
    "path": "panther/openapi/templates/spotlight.html",
    "content": "<!doctype html>\n<html>\n<head>\n  <title>Stoplight Elements with CSS</title>\n  <link rel=\"stylesheet\" href=\"https://unpkg.com/@stoplight/elements/styles.min.css\" />\n  <script type=\"module\" src=\"https://unpkg.com/@stoplight/elements/web-components.min.js\"></script>\n  <style>\n    body, html {\n      margin: 0; padding: 0; height: 100%;\n    }\n    elements-api {\n      display: block;\n      height: 100vh;\n    }\n  </style>\n</head>\n<body>\n  <elements-api id=\"api-viewer\" router=\"hash\"></elements-api>\n  <script>\n    customElements.whenDefined(\"elements-api\").then(() => {\n      document.getElementById(\"api-viewer\").apiDescriptionDocument = {{ openapi | tojson | safe }};\n    });\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "panther/openapi/templates/swagger.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Swagger UI</title>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui.css\" />\n  </head>\n  <body>\n    <div id=\"swagger-ui\"></div>\n    <script src=\"https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui-bundle.js\"></script>\n    <script>\n      SwaggerUIBundle({ spec: {{ openapi | tojson }}, dom_id: '#swagger-ui' });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "panther/openapi/urls.py",
    "content": "from panther.openapi.views import RapiDocOpenAPI, RedocOpenAPI, ScalarOpenAPI, SpotlightOpenAPI, SwaggerOpenAPI\n\nurl_routing = {\n    'scalar/': ScalarOpenAPI,\n    'swagger/': SwaggerOpenAPI,\n    'redoc/': RedocOpenAPI,\n    'rapidoc/': RapiDocOpenAPI,\n    'spotlight/': SpotlightOpenAPI,\n}\n"
  },
  {
    "path": "panther/openapi/utils.py",
    "content": "\"\"\"\nOpenAPI utilities for parsing endpoints and generating OpenAPI documentation.\n\nThis module provides classes and functions for analyzing Python endpoints\nand generating OpenAPI 3.0 specification documents.\n\"\"\"\n\nimport ast\nimport inspect\nimport logging\nimport types\nfrom typing import Any, Dict, List, Optional, Tuple, Union\n\nfrom pydantic import BaseModel\n\nfrom panther import status\nfrom panther.app import GenericAPI\nfrom panther.configs import config\n\nlogger = logging.getLogger('panther')\n\n\nclass EndpointParser:\n    \"\"\"\n    Parses endpoint functions and classes to extract response data and status codes.\n\n    This class analyzes the AST (Abstract Syntax Tree) of endpoint functions\n    to determine what data they return and what HTTP status codes they use.\n    \"\"\"\n\n    def __init__(self, endpoint: Any, http_method: str):\n        self.ast_tree = ast.parse(inspect.getsource(endpoint))\n        self.http_method = http_method\n\n        # Default values\n        self.status_code = status.HTTP_200_OK\n        self.endpoint_name = None\n        self.response_data = {}\n\n        self._parse_ast()\n\n    def _parse_ast(self) -> None:\n        \"\"\"Parse the AST to extract endpoint information.\"\"\"\n        for node in self.ast_tree.body:\n            match node:\n                case ast.ClassDef(name=class_name, body=class_body):\n                    # Handle class-based endpoints\n                    # class ...(GenericAPI):\n                    #     def get(self, ...\n                    self.endpoint_name = class_name\n                    self._parse_class_methods(class_body)\n\n                case ast.FunctionDef(name=func_name, body=func_body):\n                    # Handle function-based endpoints\n                    # def api(...\n                    self.endpoint_name = func_name\n                    self._parse_function_body(func_body)\n\n                case ast.AsyncFunctionDef(name=func_name, body=func_body):\n                    # Handle async function-based endpoints\n                    # async def api(...\n                    self.endpoint_name = func_name\n                    self._parse_function_body(func_body)\n\n    def _parse_class_methods(self, class_body: list[ast.stmt]) -> None:\n        \"\"\"Parse methods within a class-based endpoint.\"\"\"\n        for node in class_body:\n            match node:\n                case ast.FunctionDef(name=method_name, body=method_body):\n                    # def get(self, ...\n                    if method_name == self.http_method:\n                        self._parse_function_body(method_body)\n                        break\n\n                case ast.AsyncFunctionDef(name=method_name, body=method_body):\n                    # async def get(self, ...\n                    if method_name == self.http_method:\n                        self._parse_function_body(method_body)\n                        break\n\n    def _parse_function_body(self, function_body: list[ast.stmt]) -> None:\n        \"\"\"Parse the body of a function to extract return statements.\"\"\"\n        for node in function_body:\n            match node:\n                case ast.Return(value=ast.Dict(keys=dict_keys, values=dict_values)):\n                    # return {...}\n                    self.status_code = status.HTTP_200_OK\n                    self.response_data = self._extract_dict_data(dict_keys, dict_values)\n\n                case ast.Return(value=ast.Name(id=variable_name)):\n                    # return variable_name\n                    self._extract_variable_data(function_body, variable_name)\n\n                case ast.Return(value=ast.Call(args=call_args, keywords=call_keywords, func=func)):\n                    # return Response(...)\n                    self._parse_call(function_body=function_body, args=call_args, keywords=call_keywords, func=func)\n\n    def _extract_dict_data(self, keys: list[ast.expr], values: list[ast.expr]) -> dict:\n        \"\"\"Extract data from a dictionary return statement.\"\"\"\n        response = {}\n        for key, value in zip(keys, values):\n            extracted_value = None\n            match value:\n                case ast.Constant(value=constant_value):\n                    extracted_value = constant_value\n            if hasattr(key, 'value'):\n                response[key.value] = extracted_value\n        return response\n\n    def _extract_variable_data(self, function_body: list[ast.stmt], variable_name: str) -> None:\n        \"\"\"Extract data from a variable that contains a dictionary.\"\"\"\n        for variable_value in self._find_variable_assignments(function_body, variable_name):\n            match variable_value:\n                case ast.Dict(keys=dict_keys, values=dict_values):\n                    # my_response = {...}\n                    self.status_code = status.HTTP_200_OK\n                    self.response_data = self._extract_dict_data(dict_keys, dict_values)\n                case ast.List(elts=list_items):\n                    # my_response = ['1', '2']\n                    self.status_code = status.HTTP_200_OK\n                    self.response_data = []\n                    for item in list_items:\n                        match item:\n                            case ast.Constant(value=constant_value):\n                                self.response_data.append(constant_value)\n                case ast.Constant(value=constant_value):\n                    # my_response = 'Hello World!'\n                    self.response_data = constant_value\n                case ast.Tuple(elts=tuple_items):\n                    # my_response = (1, 2)\n                    self.status_code = status.HTTP_200_OK\n                    self.response_data = []\n                    for item in tuple_items:\n                        match item:\n                            case ast.Constant(value=constant_value):\n                                self.response_data.append(constant_value)\n                case ast.Call(args=call_args, keywords=call_keywords, func=func):\n                    self._parse_call(function_body=function_body, args=call_args, keywords=call_keywords, func=func)\n                case _:\n                    breakpoint()\n\n    def _parse_call(self, function_body: list[ast.stmt], args: list[ast.expr], keywords: list[ast.keyword], func):\n        if func.id == 'Response':\n            self._parse_response_call(function_body=function_body, args=args, keywords=keywords)\n        else:  # We assume this is subclass of BaseModel\n            self.status_code = status.HTTP_200_OK\n            for keyword in keywords:\n                match keyword.value:\n                    case ast.Constant(value=constant_value):\n                        # CustomBaseModel(something='Hi')\n                        self.response_data[keyword.arg] = constant_value\n                    case ast.List(elts=list_items):\n                        # CustomBaseModel(something=['1', '2'])\n                        self.response_data[keyword.arg] = []\n                        for item in list_items:\n                            match item:\n                                case ast.Constant(value=constant_value):\n                                    self.response_data[keyword.arg].append(constant_value)\n                    case ast.Dict(keys=dict_keys, values=dict_values):\n                        # CustomBaseModel(something={...})\n                        self.response_data[keyword.arg] = self._extract_dict_data(keys=dict_keys, values=dict_values)\n                    case _:\n                        logger.warning(f'Schema does not supported yet: {{{keyword.arg}: {keyword.value}}}')\n\n    def _parse_response_call(\n        self, function_body: list[ast.stmt], args: list[ast.expr], keywords: list[ast.keyword]\n    ) -> None:\n        \"\"\"Parse Response() function calls.\"\"\"\n        # Handle keyword arguments\n        for keyword in keywords:\n            if keyword.arg == 'data':\n                self._parse_data_argument(function_body, keyword.value)\n            elif keyword.arg == 'status_code':\n                self._parse_status_code_argument(function_body, keyword.value)\n\n        # Handle positional arguments\n        for index, arg in enumerate(args):\n            if index == 0:  # First argument is data\n                self._parse_data_argument(function_body=function_body, value=arg)\n            elif index == 1:  # Second argument is status_code\n                self._parse_status_code_argument(function_body=function_body, value=arg)\n\n    def _parse_status_code_argument(self, function_body: list[ast.stmt], value: ast.expr) -> None:\n        \"\"\"Parse status code from various AST patterns.\"\"\"\n        match value:\n            # return Response(?, status_code=my_status)\n            # return Response(?, my_status)\n            case ast.Name(id=variable_name):\n                for variable_value in self._find_variable_assignments(function_body, variable_name):\n                    self._extract_status_code_from_value(variable_value)\n\n            # return Response(?, status_code=status.HTTP_202_ACCEPTED)\n            # return Response(?, status.HTTP_202_ACCEPTED)\n            case ast.Attribute(value=ast.Name(id=module_name), attr=attribute_name):\n                # Handle: my_status = status.HTTP_202_ACCEPTED\n                if module_name == 'status':\n                    self.status_code = getattr(status, attribute_name)\n\n            # return Response(?, status_code=202)\n            # return Response(?, 202)\n            case ast.Constant(value=constant_value):\n                # Handle: my_status = 202\n                self.status_code = constant_value\n\n    def _parse_data_argument(self, function_body: list[ast.stmt], value: ast.expr) -> None:\n        \"\"\"Parse data argument from various AST patterns.\"\"\"\n        match value:\n            # return Response(data=my_data, ?)\n            # return Response(my_data, ?)\n            case ast.Name(id=variable_name):\n                # Handle: data=variable_name\n                for variable_value in self._find_variable_assignments(function_body, variable_name):\n                    match variable_value:\n                        # my_data = {...}\n                        case ast.Dict(keys=dict_keys, values=dict_values):\n                            self.response_data = self._extract_dict_data(dict_keys, dict_values)\n\n            # return Response(data={...}, ?)\n            # return Response({...}, ?)\n            case ast.Dict(keys=dict_keys, values=dict_values):\n                # Handle: data={...}\n                self.response_data = self._extract_dict_data(dict_keys, dict_values)\n\n    def _extract_status_code_from_value(self, value: ast.expr) -> None:\n        \"\"\"Extract status code from a variable assignment value.\"\"\"\n        match value:\n            case ast.Attribute(value=ast.Name(id=module_name), attr=attribute_name):\n                # my_status = status.HTTP_202_ACCEPTED\n                if module_name == 'status':\n                    self.status_code = getattr(status, attribute_name)\n            case ast.Constant(value=constant_value):\n                # my_status = 202\n                self.status_code = constant_value\n\n    def _find_variable_assignments(self, function_body: list[ast.stmt], variable_name: str):\n        \"\"\"Find all assignments to a specific variable name.\"\"\"\n        for node in function_body:\n            match node:\n                case ast.Assign(targets=targets, value=value):\n                    for target in targets:\n                        match target:\n                            case ast.Name(id=target_name):\n                                if target_name == variable_name:\n                                    yield value\n\n\nclass OpenAPIGenerator:\n    \"\"\"\n    Generates OpenAPI 3.0 specification documents from Panther endpoints.\n\n    This class analyzes registered endpoints and generates comprehensive\n    OpenAPI documentation including schemas, paths, and security definitions.\n    \"\"\"\n\n    HTTP_METHODS = ['post', 'get', 'put', 'patch', 'delete']\n    REQUEST_BODY_METHODS = ['post', 'put', 'patch']\n\n    @classmethod\n    def get_model_name(cls, model: type[BaseModel]) -> str:\n        \"\"\"Get the name of a model class.\"\"\"\n        if hasattr(model, '__name__'):\n            return model.__name__\n        return model.__class__.__name__\n\n    @classmethod\n    def extract_path_parameters(cls, endpoint: Any, endpoint_name: str, http_method: str) -> list[dict[str, Any]]:\n        \"\"\"\n        Extract path parameters from endpoint function signature.\n\n        Args:\n            endpoint: The endpoint function or class\n            endpoint_name: Name of the endpoint function or class\n            http_method: The HTTP method\n\n        Returns:\n            List of parameter schemas for OpenAPI\n        \"\"\"\n        param_names = []\n        for url, endpoint in config.FLAT_URLS.items():\n            if endpoint.__name__ == endpoint_name:\n                for part in url.split('/'):\n                    if part.startswith('<'):\n                        param_names.append(part.strip('< >'))\n        if not param_names:\n            return []\n\n        signature = cls._get_function_signature(endpoint, http_method)\n        parameters = []\n        for param_name in param_names:\n            if param_name not in signature.parameters:\n                parameters.append({'name': param_name, 'in': 'path', 'required': True, 'schema': {'type': 'string'}})\n            else:\n                param_schema = cls._create_parameter_schema(param_name, signature.parameters[param_name])\n                parameters.append(param_schema)\n\n        return parameters\n\n    @classmethod\n    def _get_function_signature(cls, endpoint: Any, http_method: str):\n        \"\"\"Get the function signature for an endpoint.\"\"\"\n        if isinstance(endpoint, types.FunctionType):\n            func = endpoint\n        else:\n            func = getattr(endpoint, http_method)\n        return inspect.signature(obj=func)\n\n    @classmethod\n    def _create_parameter_schema(cls, param_name: str, param_info: inspect.Parameter) -> dict[str, Any]:\n        \"\"\"Create OpenAPI parameter schema from function parameter.\"\"\"\n        param_schema = {'name': param_name, 'in': 'path', 'required': True, 'schema': {'type': 'string'}}\n\n        # Map Python types to OpenAPI types\n        if param_info.annotation is int:\n            param_schema['schema']['type'] = 'integer'\n        elif param_info.annotation is bool:\n            param_schema['schema']['type'] = 'boolean'\n        elif param_info.annotation is float:\n            param_schema['schema']['type'] = 'number'\n\n        return param_schema\n\n    @classmethod\n    def parse_docstring(cls, docstring: str) -> tuple[str, str]:\n        \"\"\"Parse docstring into summary and description.\"\"\"\n        if not docstring:\n            return '', ''\n\n        lines = docstring.strip().split('\\n')\n        summary = lines[0]\n        description = '<br>'.join(lines[1:]).strip() if len(lines) > 1 else ''\n\n        return summary, description\n\n    @classmethod\n    def extract_field_constraints(cls, field: Any) -> dict[str, Any]:\n        \"\"\"Extract validation constraints from Pydantic field.\"\"\"\n        constraint_attributes = ['min_length', 'max_length', 'regex', 'ge', 'le', 'gt', 'lt']\n\n        constraints = {}\n        for attr in constraint_attributes:\n            value = getattr(field, attr, None)\n            if value is not None:\n                constraints[attr] = value\n\n        return constraints\n\n    @classmethod\n    def enrich_schema_with_constraints(cls, schema: dict[str, Any], model: Any) -> dict[str, Any]:\n        \"\"\"Add field constraints to OpenAPI schema.\"\"\"\n        if 'properties' not in schema:\n            return schema\n\n        for field_name, field in model.model_fields.items():\n            if field_name in schema['properties']:\n                constraints = cls.extract_field_constraints(field)\n                schema['properties'][field_name].update(constraints)\n\n        return schema\n\n    @classmethod\n    def generate_operation_content(cls, endpoint: Any, http_method: str, schemas: dict[str, Any]) -> dict[str, Any]:\n        \"\"\"Generate OpenAPI operation content for an endpoint.\"\"\"\n        # Skip if endpoint is excluded from docs\n        if endpoint.output_schema and endpoint.output_schema.exclude_in_docs:\n            return {}\n\n        # Parse endpoint response\n        response_parser = EndpointParser(endpoint, http_method)\n\n        # Extract basic operation info\n        operation_id = f'{response_parser.endpoint_name}_{http_method}'\n        parameters = cls.extract_path_parameters(\n            endpoint=endpoint, endpoint_name=response_parser.endpoint_name, http_method=http_method\n        )\n        summary, description = cls.parse_docstring(endpoint.__doc__)\n\n        # Extract tags\n        tags = cls._extract_operation_tags(endpoint=endpoint, response_parser=response_parser)\n\n        # Extract metadata\n        metadata = cls._extract_endpoint_metadata(endpoint=endpoint, description=description)\n\n        # Handle response schema\n        response_schema = cls._build_response_schema(\n            endpoint=endpoint, response_parser=response_parser, schemas=schemas\n        )\n\n        # Handle request body\n        request_body = cls._build_request_body(endpoint=endpoint, http_method=http_method, schemas=schemas)\n\n        # Build operation content\n        operation_content = {\n            'operationId': operation_id,\n            'summary': summary,\n            'description': metadata['description'],\n            'tags': tags,\n            'parameters': parameters,\n            'security': metadata['security'],\n            'deprecated': metadata['deprecated'],\n        }\n\n        operation_content.update(response_schema)\n        operation_content.update(request_body)\n\n        return {http_method: operation_content}\n\n    @classmethod\n    def _extract_operation_tags(cls, endpoint: Any, response_parser: EndpointParser) -> list[str]:\n        \"\"\"Extract tags for operation grouping.\"\"\"\n        if endpoint.output_schema and endpoint.output_schema.tags:\n            return endpoint.output_schema.tags\n        return [response_parser.endpoint_name] if response_parser.endpoint_name else [endpoint.__module__]\n\n    @classmethod\n    def _extract_endpoint_metadata(cls, endpoint: Any, description: str) -> dict[str, Any]:\n        \"\"\"Extract metadata like permissions, throttling, etc.\"\"\"\n        # Extract permissions\n        permissions = [p.__name__ for p in endpoint.permissions] if endpoint.permissions else None\n\n        # Extract throttling\n        throttling = None\n        if endpoint.throttling:\n            throttling = f'{endpoint.throttling.rate} per {endpoint.throttling.duration}'\n\n        # Extract cache\n        cache = str(endpoint.cache) if endpoint.cache else None\n\n        # Extract middlewares\n        middlewares = None\n        if endpoint.middlewares:\n            middlewares = [getattr(m, '__name__', str(m)) for m in endpoint.middlewares]\n\n        # Extract deprecated status\n        deprecated = endpoint.output_schema.deprecated if endpoint.output_schema else False\n\n        # Build security\n        security = [{'BearerAuth': []}] if endpoint.auth else []\n\n        # Enhance description with metadata\n        if permissions:\n            description += f'<br>  - Permissions: {permissions}'\n        if throttling:\n            description += f'<br>  - Throttling: {throttling}'\n        if cache:\n            description += f'<br>  - Cache: {cache}'\n        if middlewares:\n            description += f'<br>  - Middlewares: {middlewares}'\n\n        return {\n            'description': description,\n            'security': security,\n            'deprecated': deprecated,\n        }\n\n    @classmethod\n    def _build_response_schema(\n        cls, endpoint: Any, response_parser: EndpointParser, schemas: dict[str, Any]\n    ) -> dict[str, Any]:\n        \"\"\"Build response schema for the endpoint.\"\"\"\n        if endpoint.output_schema:\n            status_code = endpoint.output_schema.status_code\n            model = endpoint.output_schema.model\n        elif endpoint.output_model:\n            status_code = response_parser.status_code\n            model = endpoint.output_model\n        else:\n            status_code = response_parser.status_code\n            schema_ref = {'properties': {k: {'default': v} for k, v in response_parser.response_data.items()}}\n            return {'responses': {status_code: {'content': {'application/json': {'schema': schema_ref}}}}}\n\n        # Add model to schemas if not present\n        model_name = cls.get_model_name(model)\n        if model_name not in schemas:\n            schema = model.schema(ref_template='#/components/schemas/{model}')\n            schema = cls.enrich_schema_with_constraints(schema, model)\n            schemas[model_name] = schema\n\n        schema_ref = {'$ref': f'#/components/schemas/{model_name}'}\n\n        # Build responses\n        responses = {'responses': {status_code: {'content': {'application/json': {'schema': schema_ref}}}}}\n\n        # Add error responses\n        if error_responses := cls._build_error_responses(endpoint):\n            responses['responses'] |= error_responses\n\n        return responses\n\n    @classmethod\n    def _build_error_responses(cls, endpoint: Any) -> dict[int, dict[str, str]]:\n        \"\"\"Build standard error responses for the endpoint.\"\"\"\n        error_responses = {}\n\n        if endpoint.auth:\n            error_responses[401] = {'description': 'Unauthorized'}\n        if endpoint.permissions:\n            error_responses[403] = {'description': 'Forbidden'}\n        if endpoint.input_model:\n            error_responses[400] = {'description': 'Bad Request'}\n            error_responses[422] = {'description': 'Unprocessable Entity'}\n\n        return error_responses\n\n    @classmethod\n    def _build_request_body(cls, endpoint: Any, http_method: str, schemas: dict[str, Any]) -> dict[str, Any]:\n        \"\"\"Build request body schema for the endpoint.\"\"\"\n        if not (endpoint.input_model and http_method in cls.REQUEST_BODY_METHODS):\n            return {}\n\n        model = endpoint.input_model\n        model_name = cls.get_model_name(model)\n\n        # Add model to schemas if not present\n        if model_name not in schemas:\n            schema = model.schema(ref_template='#/components/schemas/{model}')\n            schema = cls.enrich_schema_with_constraints(schema, model)\n            schemas[model_name] = schema\n\n        return {\n            'requestBody': {\n                'required': True,\n                'content': {'application/json': {'schema': {'$ref': f'#/components/schemas/{model_name}'}}},\n            }\n        }\n\n    @classmethod\n    def generate_openapi_spec(cls) -> dict[str, Any]:\n        \"\"\"\n        Generate complete OpenAPI 3.0 specification.\n\n        Returns:\n            Complete OpenAPI specification dictionary\n        \"\"\"\n        paths = {}\n        schemas = {}\n\n        # Process all registered endpoints\n        for url, endpoint in config.FLAT_URLS.items():\n            url = url.replace('<', '{').replace('>', '}')\n            if not url.startswith('/'):\n                url = f'/{url}'\n            paths[url] = {}\n\n            if isinstance(endpoint, types.FunctionType):\n                # Function-based endpoints\n                for method in cls.HTTP_METHODS:\n                    if method.upper() in endpoint.methods:\n                        paths[url].update(cls.generate_operation_content(endpoint, method, schemas))\n            else:\n                # Class-based endpoints\n                for method in cls.HTTP_METHODS:\n                    # Check if method is overridden (not the default GenericAPI method)\n                    if getattr(endpoint, method) is not getattr(GenericAPI, method):\n                        paths[url].update(cls.generate_operation_content(endpoint, method, schemas))\n\n        # Build security schemes\n        security_schemes = {\n            'BearerAuth': {\n                'type': 'http',\n                'scheme': 'bearer',\n                'bearerFormat': 'JWT',\n            }\n        }\n\n        # Build complete OpenAPI specification\n        openapi_spec = {\n            'openapi': '3.0.0',\n            'info': {\n                'title': 'Panther API',\n                'version': '1.0.0',\n                'description': 'Auto-generated OpenAPI documentation for Panther project.',\n            },\n            'paths': paths,\n            'components': {'schemas': schemas, 'securitySchemes': security_schemes},\n            'security': [{'BearerAuth': []}],\n        }\n\n        return openapi_spec\n"
  },
  {
    "path": "panther/openapi/views.py",
    "content": "from panther.app import GenericAPI\nfrom panther.openapi import OutputSchema\nfrom panther.openapi.utils import OpenAPIGenerator\nfrom panther.response import TemplateResponse\n\n\nclass ScalarOpenAPI(GenericAPI):\n    output_schema = OutputSchema(exclude_in_docs=True)\n\n    def get(self):\n        return TemplateResponse(name='scalar.html', context={'openapi': OpenAPIGenerator.generate_openapi_spec()})\n\n\nclass SwaggerOpenAPI(GenericAPI):\n    output_schema = OutputSchema(exclude_in_docs=True)\n\n    def get(self):\n        return TemplateResponse(name='swagger.html', context={'openapi': OpenAPIGenerator.generate_openapi_spec()})\n\n\nclass RedocOpenAPI(GenericAPI):\n    output_schema = OutputSchema(exclude_in_docs=True)\n\n    def get(self):\n        return TemplateResponse(name='redoc.html', context={'openapi': OpenAPIGenerator.generate_openapi_spec()})\n\n\nclass RapiDocOpenAPI(GenericAPI):\n    output_schema = OutputSchema(exclude_in_docs=True)\n\n    def get(self):\n        return TemplateResponse(name='rapidoc.html', context={'openapi': OpenAPIGenerator.generate_openapi_spec()})\n\n\nclass SpotlightOpenAPI(GenericAPI):\n    output_schema = OutputSchema(exclude_in_docs=True)\n\n    def get(self):\n        return TemplateResponse(name='spotlight.html', context={'openapi': OpenAPIGenerator.generate_openapi_spec()})\n"
  },
  {
    "path": "panther/pagination.py",
    "content": "from pantherdb import Cursor as PantherDBCursor\n\nfrom panther.db.cursor import Cursor\n\n\nclass Pagination:\n    \"\"\"\n    Request URL:\n        example.com/users?limit=10&skip=0\n    Response Data:\n        {\n            'count': 10,\n            'next': '?limit=10&skip=10',\n            'previous': None,\n            'results': [...]\n        }\n    \"\"\"\n\n    DEFAULT_LIMIT = 20\n    DEFAULT_SKIP = 0\n\n    def __init__(self, query_params: dict, cursor: Cursor | PantherDBCursor):\n        self.limit = self.get_limit(query_params=query_params)\n        self.skip = self.get_skip(query_params=query_params)\n        self.cursor = cursor\n\n    def get_limit(self, query_params: dict) -> int:\n        return int(query_params.get('limit', self.DEFAULT_LIMIT))\n\n    def get_skip(self, query_params: dict) -> int:\n        return int(query_params.get('skip', self.DEFAULT_SKIP))\n\n    def build_next_params(self):\n        next_skip = self.skip + self.limit\n        return f'?limit={self.limit}&skip={next_skip}'\n\n    def build_previous_params(self):\n        previous_skip = max(self.skip - self.limit, 0)\n        return f'?limit={self.limit}&skip={previous_skip}'\n\n    def paginate(self):\n        return self.cursor.skip(skip=self.skip).limit(limit=self.limit)\n\n    async def template(self, response: list):\n        count = await self.cursor.cls.count(self.cursor.filter)\n        has_next = not bool(self.limit + self.skip >= count)\n\n        return {\n            'count': count,\n            'next': self.build_next_params() if has_next else None,\n            'previous': self.build_previous_params() if self.skip else None,\n            'results': response,\n        }\n"
  },
  {
    "path": "panther/panel/__init__.py",
    "content": ""
  },
  {
    "path": "panther/panel/authentications.py",
    "content": "from panther.authentications import CookieJWTAuthentication\nfrom panther.exceptions import AuthenticationAPIError, RedirectAPIError\nfrom panther.request import Request\n\n\nclass AdminCookieJWTAuthentication(CookieJWTAuthentication):\n    async def __call__(self, request: Request):\n        try:\n            return await super().__call__(request=request)\n        except AuthenticationAPIError:\n            raise RedirectAPIError(url=f'login?redirect_to={request.path}')\n"
  },
  {
    "path": "panther/panel/middlewares.py",
    "content": "from panther.middlewares import HTTPMiddleware\nfrom panther.request import Request\nfrom panther.response import RedirectResponse\n\n\nclass RedirectToSlashMiddleware(HTTPMiddleware):\n    async def __call__(self, request: Request):\n        if not request.path.endswith('/'):\n            return RedirectResponse(request.path + '/')\n        return await self.dispatch(request=request)\n"
  },
  {
    "path": "panther/panel/permissions.py",
    "content": "from panther.exceptions import RedirectAPIError\nfrom panther.permissions import BasePermission\nfrom panther.request import Request\n\n\nclass IsAuthenticated(BasePermission):\n    async def __call__(self, request: Request):\n        if request.user is None:\n            raise RedirectAPIError(url=f'login?redirect_to={request.path}')\n"
  },
  {
    "path": "panther/panel/templates/base.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <title>{{ title }}</title>\n  <script src=\"https://cdn.tailwindcss.com\"></script>\n</head>\n<body class=\"bg-gray-900 text-white p-8\">\n{% block content %}\n{% endblock %}\n</body>\n</html>\n"
  },
  {
    "path": "panther/panel/templates/create.html",
    "content": "{% extends \"base.html\" %}\n\n{% block content %}\n<div class=\"max-w-4xl mx-auto\">\n  <h1 class=\"text-2xl font-semibold mb-6\">Create New Record</h1>\n\n  <form id=\"createForm\" class=\"space-y-4 bg-gray-800 p-6 rounded-lg\">\n    <div id=\"dynamicInputs\" class=\"space-y-4\">\n      <!-- Dynamic inputs will be generated here -->\n    </div>\n    <button type=\"submit\" class=\"w-full bg-blue-600 hover:bg-blue-500 text-white py-2 px-4 rounded-lg\">\n      Submit\n    </button>\n  </form>\n</div>\n<script> \nconst isUpdate = false; \n  const data = {};\n  {% include \"create.js\" %}\n</script>\n{% endblock %}\n"
  },
  {
    "path": "panther/panel/templates/create.js",
    "content": "const schema = JSON.parse(`{{fields|tojson|safe}}`);\nfunction toggleObjectVisibility(checkbox, contentId) {\n  const content = document.getElementById(contentId);\n  content.classList.toggle(\"hidden\", !checkbox.checked);\n\n  // Disable/enable all inputs within the container\n  const inputs = content.querySelectorAll(\"input, select, textarea\");\n  inputs.forEach((input) => {\n    input.disabled = !checkbox.checked;\n  });\n}\n\nfunction createObjectInputs(objectSchema, container, prefix = \"\") {\n  if (!objectSchema || !objectSchema.fields) return;\n\n  Object.entries(objectSchema.fields).forEach(([fieldName, field]) => {\n    const fullFieldName = prefix ? `${prefix}.${fieldName}` : fieldName;\n\n    // Check if it's an array type\n    if (field.type.includes(\"array\")) {\n      // If items is specified, use it\n      if (field.items) {\n        const itemType = field.items.replace(\"$\", \"\");\n        createArrayField(fieldName, itemType, container, fullFieldName, field);\n      } else {\n        // Handle array without items specification (simple types)\n        createSimpleArrayField(fieldName, container, fullFieldName, field);\n      }\n    } else if (\n      Array.isArray(field.type) &&\n      field.type.some((t) => t.startsWith(\"$\"))\n    ) {\n      const objectType = field.type\n        .find((t) => t.startsWith(\"$\"))\n        .replace(\"$\", \"\");\n      createNestedObjectField(\n        fieldName,\n        objectType,\n        field,\n        container,\n        fullFieldName\n      );\n    } else {\n      createBasicInput(fieldName, field, container, fullFieldName);\n    }\n  });\n}\n\nfunction toggleArrayVisibility(checkbox, contentId) {\n  const content = document.getElementById(contentId);\n  if (content) {\n    content.classList.toggle(\"hidden\", !checkbox.checked);\n\n    // Also enable/disable inputs\n    const inputs = content.querySelectorAll(\"input, select, textarea\");\n    inputs.forEach((input) => {\n      input.disabled = !checkbox.checked;\n    });\n  }\n}\n\nfunction createSimpleArrayField(fieldName, container, fullFieldName, field) {\n  const arrayContainer = document.createElement(\"div\");\n  arrayContainer.className = \"border border-gray-400 p-4 rounded-lg space-y-2\";\n\n  const spreadsheetId = `${fullFieldName}-container`;\n\n  const header = document.createElement(\"div\");\n  header.className = \"flex items-center justify-between mb-4\";\n  header.innerHTML = `\n    <h3 class=\"text-lg font-medium\">${field.title || fieldName}</h3>\n    ${\n      field.type.includes(\"null\")\n        ? `\n      <label class=\"flex items-center space-x-3\">\n        <input type=\"checkbox\" \n               id=\"${fullFieldName}_toggle\"\n               class=\"form-checkbox h-5 w-5 text-blue-600 bg-gray-700 border-gray-600 rounded\"\n               ${!field.required ? \"\" : \"checked disabled\"}\n               onchange=\"toggleArrayVisibility(this, '${spreadsheetId}')\">\n        <span class=\"text-sm font-medium\">Include ${\n          field.title || fieldName\n        }</span>\n      </label>\n    `\n        : \"\"\n    }\n  `;\n\n  const content = document.createElement(\"div\");\n  content.innerHTML = `\n    <div class=\"flex justify-between items-center mb-4\">\n      <button type=\"button\" \n              class=\"bg-green-600 px-3 py-1 rounded text-sm\"\n              onclick=\"addSimpleArrayRow('${fullFieldName}', '${spreadsheetId}')\">\n        Add Item\n      </button>\n    </div>\n    <div id=\"${spreadsheetId}\" class=\"array-items space-y-4\">\n    </div>\n  `;\n\n  if (field.type.includes(\"null\") && !field.required) {\n    content.classList.add(\"hidden\");\n  }\n\n  arrayContainer.appendChild(header);\n  arrayContainer.appendChild(content);\n  container.appendChild(arrayContainer);\n}\n\nfunction addSimpleArrayRow(arrayName, containerId) {\n  const container = document.getElementById(containerId);\n  if (!container) {\n    console.error(\"Invalid container\");\n    return;\n  }\n\n  const rowIndex = container.children.length;\n  const rowContainer = document.createElement(\"div\");\n  rowContainer.className = \"flex items-center space-x-4\";\n\n  const input = document.createElement(\"input\");\n  input.type = \"text\";\n  input.name = `${arrayName}[${rowIndex}]`;\n  input.className =\n    \"flex-grow bg-gray-700 border border-gray-600 rounded px-3 py-2\";\n\n  const deleteButton = document.createElement(\"button\");\n  deleteButton.className = \"bg-red-600 px-3 py-1 rounded text-sm\";\n  deleteButton.textContent = \"Delete\";\n  deleteButton.onclick = () => {\n    rowContainer.remove();\n    reindexArrayItems(containerId);\n  };\n  rowContainer.appendChild(input);\n  rowContainer.appendChild(deleteButton);\n  container.appendChild(rowContainer);\n}\n\nfunction reindexArrayItems(containerId) {\n  const container = document.getElementById(containerId);\n  const items = container.children;\n\n  // Update the index for each remaining item\n  Array.from(items).forEach((item, newIndex) => {\n    const input = item.querySelector(\"input\");\n    if (input) {\n      const oldName = input.name;\n      const baseName = oldName.split(\"[\")[0];\n      input.name = `${baseName}[${newIndex}]`;\n    }\n  });\n}\nfunction createNestedObjectField(\n  fieldName,\n  objectType,\n  field,\n  container,\n  fullFieldName\n) {\n  const objectWrapper = document.createElement(\"div\");\n  objectWrapper.className = \"space-y-4 border border-gray-400 p-4 rounded-lg\";\n\n  const cleanObjectType = objectType.replace(/^\\$/, \"\");\n\n  const header = document.createElement(\"div\");\n  header.className = \"flex items-center justify-between mb-4\";\n  header.innerHTML = `<h3 class=\"text-lg font-medium\">${\n    field.title || fieldName\n  }</h3>`;\n\n  const contentContainer = document.createElement(\"div\");\n  contentContainer.id = `${fullFieldName}_content`;\n  contentContainer.className = \"space-y-4\";\n\n  // Add toggle for optional objects\n  if (!field.required) {\n    const toggleContainer = document.createElement(\"div\");\n    toggleContainer.className = \"flex items-center space-x-3\";\n\n    const checkbox = document.createElement(\"input\");\n    checkbox.type = \"checkbox\";\n    checkbox.id = `${fullFieldName}_toggle`;\n    checkbox.className = \"form-checkbox h-5 w-5 text-blue-600\";\n\n    const label = document.createElement(\"label\");\n    label.htmlFor = `${fullFieldName}_toggle`;\n    label.textContent = `Include ${field.title || fieldName}`;\n\n    toggleContainer.appendChild(checkbox);\n    toggleContainer.appendChild(label);\n    header.appendChild(toggleContainer);\n\n    // Toggle handler to enable/disable validation\n    checkbox.addEventListener(\"change\", (e) => {\n      const fields = contentContainer.querySelectorAll(\n        \"input, select, textarea\"\n      );\n      fields.forEach((field) => {\n        if (!e.target.checked) {\n          // When unchecked, disable and remove required attribute\n          field.disabled = true;\n          field.required = false;\n          field.value = \"\"; // Clear the value\n        } else {\n          // When checked, enable and restore required if it was originally required\n          field.disabled = false;\n          field.required = field.dataset.originalRequired === \"true\";\n        }\n      });\n    });\n\n    // Initialize as unchecked\n    checkbox.checked = false;\n    setTimeout(() => checkbox.dispatchEvent(new Event(\"change\")), 0);\n  }\n\n  const nestedSchema = schema.$[cleanObjectType];\n  createObjectInputs(nestedSchema, contentContainer, fullFieldName);\n\n  // Store original required state for all nested fields\n  contentContainer\n    .querySelectorAll(\"input, select, textarea\")\n    .forEach((field) => {\n      field.dataset.originalRequired = field.required;\n      if (!field.required) {\n        field.required = false;\n      }\n    });\n\n  objectWrapper.appendChild(header);\n  objectWrapper.appendChild(contentContainer);\n  container.appendChild(objectWrapper);\n}\n\nfunction createObjectInputs(objectSchema, container, prefix = \"\") {\n  if (!objectSchema || !objectSchema.fields) return;\n\n  Object.entries(objectSchema.fields).forEach(([fieldName, field]) => {\n    const fullFieldName = prefix ? `${prefix}.${fieldName}` : fieldName;\n\n    // Check if it's an array type\n    if (field.type.includes(\"array\")) {\n      // If items is specified, use it\n      if (field.items) {\n        const itemType = field.items.replace(\"$\", \"\");\n        createArrayField(fieldName, itemType, container, fullFieldName, field);\n      } else {\n        // Handle array without items specification (simple types)\n        createSimpleArrayField(fieldName, container, fullFieldName, field);\n      }\n    } else if (\n      // Check if type is an array and contains a reference to another type\n      (Array.isArray(field.type) &&\n        field.type.some((t) => t.startsWith(\"$\"))) ||\n      // Or if type is a string and is a reference\n      (typeof field.type === \"string\" && field.type.startsWith(\"$\"))\n    ) {\n      const objectType = Array.isArray(field.type)\n        ? field.type.find((t) => t.startsWith(\"$\"))\n        : field.type;\n      createNestedObjectField(\n        fieldName,\n        objectType,\n        field,\n        container,\n        fullFieldName\n      );\n    } else {\n      createBasicInput(fieldName, field, container, fullFieldName);\n    }\n  });\n}\n\nfunction createBasicInput(fieldName, field, container, fullFieldName) {\n  const inputWrapper = document.createElement(\"div\");\n  inputWrapper.className = \"space-y-2\";\n  if (fieldName === \"id\" && typeof isUpdate === \"undefined\") {\n    return;\n  }\n\n  // Hide ID field in update mode\n  if (fieldName === \"_id\" && isUpdate) {\n    const inputWrapper = document.createElement(\"div\");\n    inputWrapper.className = \"space-y-2\";\n    inputWrapper.style.display = \"none\"; // Hide the entire wrapper\n\n    inputWrapper.innerHTML = `\n      <label class=\"block\">\n        <span class=\"text-sm font-medium\">ID</span>\n        <input type=\"text\" name=\"_id\" readonly\n               class=\"w-full mt-1 p-2 bg-slate-900 rounded text-gray-300\">\n      </label>\n    `;\n\n    container.appendChild(inputWrapper);\n    return;\n  }\n\n  let inputHTML = \"\";\n  const defaultValue =\n    field.default !== undefined ? `value=\"${field.default}\"` : \"\";\n  const requiredText = field.required\n    ? `<span class=\"text-red-500 text-sm ml-2\">* Required</span>`\n    : \"\";\n  if (field.type.includes(\"boolean\")) {\n    inputHTML = `\n        <label class=\"flex items-center space-x-3\">\n          <input type=\"checkbox\" name=\"${fullFieldName}\"\n                 ${field.default ? \"checked\" : \"\"}\n                 class=\"form-checkbox h-5 w-5 text-blue-600 bg-gray-700 border-gray-600 rounded\">\n          <span class=\"text-sm font-medium\">${field.title || fieldName}</span>\n          ${requiredText}\n        </label>\n      `;\n  } else if (field.type.includes(\"string\")) {\n    inputHTML = `\n      <label class=\"block\">\n        <span class=\"text-sm font-medium\">${\n          field.title || fieldName\n        } ${requiredText}</span>\n        <input type=\"text\" name=\"${fullFieldName}\"\n               ${defaultValue}\n               ${field.required ? \"required\" : \"\"}\n               class=\"w-full mt-1 p-2 bg-slate-900 rounded text-gray-300\">\n      </label>\n    `;\n  } else if (field.type.includes(\"integer\")) {\n    const min = field.min !== undefined ? `min=\"${field.min}\"` : \"\";\n    const max = field.max !== undefined ? `max=\"${field.max}\"` : \"\";\n\n    inputHTML = `\n      <label class=\"block\">\n        <span class=\"text-sm font-medium\">${\n          field.title || fieldName\n        } ${requiredText}</span>\n        <input type=\"number\" name=\"${fullFieldName}\"\n               ${defaultValue} ${min} ${max}\n               ${field.required ? \"required\" : \"\"}\n               class=\"w-full mt-1 p-2 bg-slate-900 rounded text-gray-300\">\n      </label>\n    `;\n  }\n\n  inputWrapper.innerHTML = inputHTML;\n  container.appendChild(inputWrapper);\n}\n\nfunction createArrayField(fieldName, itemType, container, fullFieldName) {\n  const arrayContainer = document.createElement(\"div\");\n  arrayContainer.className = \"border border-gray-700 p-4 rounded-lg space-y-4\";\n\n  const spreadsheetId = `${fullFieldName}-container`;\n\n  // Make sure itemType exists in schema.$\n  if (!schema.$ || !schema.$[itemType]) {\n    console.error(`Schema type ${itemType} not found`);\n    return;\n  }\n\n  arrayContainer.innerHTML = `\n    <div class=\"flex justify-between items-center mb-4\">\n      <h3 class=\"text-lg font-medium\">${fieldName}</h3>\n      <button type=\"button\" \n              class=\"bg-green-600 px-3 py-1 rounded text-sm\"\n              onclick=\"addArrayRow('${fullFieldName}', '${itemType}', '${spreadsheetId}')\">\n        Add Item\n      </button>\n    </div>\n    <div id=\"${spreadsheetId}\" class=\"array-items space-y-4\"></div>\n  `;\n\n  container.appendChild(arrayContainer);\n}\n\nfunction addArrayRow(arrayName, itemType, containerId) {\n  const container = document.getElementById(containerId);\n  if (!container || !schema.$ || !schema.$[itemType]) {\n    console.error(\"Invalid container or schema type\");\n    return;\n  }\n\n  const itemSchema = schema.$[itemType];\n  const rowIndex = container.children.length;\n  const rowContainer = document.createElement(\"div\");\n  rowContainer.className =\n    \"flex  gap-2 items-start space-x-4 py-4 pl-3 border pr-2 bg-gray-800/80 border-gray-500 rounded-lg\";\n\n  const itemContent = document.createElement(\"div\");\n  itemContent.className = \"flex-grow flex flex-col gap-8 w-full\";\n\n  createObjectInputs(itemSchema, itemContent, `${arrayName}[${rowIndex}]`);\n\n  const deleteButton = document.createElement(\"button\");\n  deleteButton.className = \"bg-red-600 m-1 p-1 min-w-8 min-h-8 rounded text-sm\";\n  deleteButton.textContent = \"X\";\n  deleteButton.onclick = () => {\n    rowContainer.remove();\n    reindexComplexArrayItems(containerId);\n  };\n  rowContainer.appendChild(itemContent);\n  rowContainer.appendChild(deleteButton);\n  container.appendChild(rowContainer);\n}\nfunction reindexComplexArrayItems(containerId) {\n  const container = document.getElementById(containerId);\n  const items = container.children;\n\n  Array.from(items).forEach((item, newIndex) => {\n    const inputs = item.querySelectorAll(\"input, select, textarea\");\n    inputs.forEach((input) => {\n      const oldName = input.name;\n      const fieldPart = oldName.split(\"]\")[1]; // Get the part after the index\n      const baseName = oldName.split(\"[\")[0];\n      input.name = `${baseName}[${newIndex}]${fieldPart || \"\"}`;\n    });\n  });\n}\nfunction openObjectModal(fieldName, objectType) {\n  // Create modal for editing nested object\n  const modal = document.createElement(\"div\");\n  modal.className =\n    \"fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center\";\n  modal.innerHTML = `\n<div class=\"bg-gray-800 p-6 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto\">\n  <h3 class=\"text-lg font-medium mb-4\">Edit ${fieldName}</h3>\n  <div class=\"object-inputs space-y-4\"></div>\n  <div class=\"mt-4 flex justify-end space-x-2\">\n    <button type=\"button\" class=\"bg-gray-600 px-4 py-2 rounded\" onclick=\"this.closest('.fixed').remove()\">\n      Cancel\n    </button>\n    <button type=\"button\" class=\"bg-blue-600 px-4 py-2 rounded\" onclick=\"saveObjectModal(this)\">\n      Save\n    </button>\n  </div>\n</div>\n`;\n\n  const objectSchema = schema.$[objectType];\n  createObjectInputs(\n    objectSchema,\n    modal.querySelector(\".object-inputs\"),\n    fieldName\n  );\n\n  document.body.appendChild(modal);\n}\n\nconst dynamicInputs = document.getElementById(\"dynamicInputs\");\n\ncreateObjectInputs(schema, dynamicInputs);\n\n// Check if the page is in update mode\nif (typeof isUpdate !== \"undefined\" && isUpdate) {\n  // Populate the form with existing data for update mode\n  populateFormWithExistingData(existingData);\n} else {\n  console.log(\"Create mode: No existing data to populate.\");\n}\n\n// Function to populate the form with existing data\nfunction populateFormWithExistingData(data) {\n  console.log(\"Populating form with data:\", data);\n\n  // Handle simple fields first\n  Object.entries(data).forEach(([key, value]) => {\n    const input = document.querySelector(`[name=\"${key}\"]`);\n    if (input) {\n      if (input.type === \"checkbox\") {\n        input.checked = Boolean(value);\n      } else {\n        input.value = value !== null ? value : \"\";\n      }\n    }\n  });\n\n  // Special case for the `_id` field\n  // Find the ID input field\n  const idInput = document.querySelector(`[name=\"_id\"]`);\n  if (idInput) {\n    // If we're in update mode and have an ID, set the value\n    if (data && data.id) {\n      idInput.value = data.id;\n    }\n\n    // Hide the input itself\n    idInput.style.display = \"none\";\n\n    // Also hide the parent container (which likely contains the label)\n    const parentContainer =\n      idInput.closest(\".form-group\") || idInput.parentElement;\n    if (parentContainer) {\n      parentContainer.style.display = \"none\";\n    }\n\n    // Keep the field in the form data by making it readonly but not disabled\n    idInput.setAttribute(\"readonly\", true);\n  }\n\n  // Handle nested objects and arrays\n  populateNestedData(data);\n}\n\nfunction populateNestedData(data, prefix = \"\") {\n  Object.entries(data).forEach(([key, value]) => {\n    const fullPath = prefix ? `${prefix}.${key}` : key;\n\n    // Handle arrays\n    if (Array.isArray(value)) {\n      // Find the array container\n      const containerID = `${fullPath}-container`;\n      const container = document.getElementById(containerID);\n\n      if (container) {\n        // Ensure toggle is checked for this array if it exists\n        const toggle = document.getElementById(`${fullPath}_toggle`);\n        if (toggle) {\n          toggle.checked = true;\n          toggleArrayVisibility(toggle, containerID);\n        }\n\n        // Clear any existing items\n        container.innerHTML = \"\";\n\n        // Recreate each array item\n        value.forEach((item, index) => {\n          if (typeof item === \"object\" && item !== null) {\n            // Complex object in array\n            const itemType = determineItemType(item);\n            if (itemType) {\n              addArrayRow(fullPath, itemType, containerID);\n\n              // After adding the row, populate its fields\n              // The new row should be the last child of the container\n              const newRow = container.lastElementChild;\n              if (newRow) {\n                // Find all inputs in this row and set their values\n                Object.entries(item).forEach(([itemKey, itemValue]) => {\n                  const itemPath = `${fullPath}[${index}].${itemKey}`;\n                  populateItemField(newRow, itemPath, itemValue);\n                });\n              }\n            }\n          } else {\n            // Simple value in array\n            addSimpleArrayRow(fullPath, containerID);\n            // Get the last added row and set its value\n            const rowInput = container.lastElementChild.querySelector(\"input\");\n            if (rowInput) {\n              rowInput.value = item;\n            }\n          }\n        });\n      }\n    }\n    // Handle nested objects\n    else if (typeof value === \"object\" && value !== null) {\n      // Find the content container for this object\n      const contentId = `${fullPath}_content`;\n      const content = document.getElementById(contentId);\n\n      if (content) {\n        // Enable the toggle if it exists\n        const toggle = document.getElementById(`${fullPath}_toggle`);\n        if (toggle) {\n          toggle.checked = true;\n          // Manually enable all fields in this container\n          const inputs = content.querySelectorAll(\"input, select, textarea\");\n          inputs.forEach((input) => {\n            input.disabled = false;\n          });\n        }\n\n        // Populate the fields in this object\n        Object.entries(value).forEach(([nestedKey, nestedValue]) => {\n          const nestedPath = `${fullPath}.${nestedKey}`;\n          populateItemField(content, nestedPath, nestedValue);\n        });\n      }\n    }\n  });\n}\n\nfunction populateItemField(container, fieldPath, value) {\n  const input = container.querySelector(`[name=\"${fieldPath}\"]`);\n  if (input) {\n    if (input.type === \"checkbox\") {\n      input.checked = Boolean(value);\n    } else {\n      input.value = value !== null ? value : \"\";\n    }\n  }\n}\n\nfunction determineItemType(item) {\n  // Try to match the item structure with schema definitions\n  for (const [typeName, typeSchema] of Object.entries(schema.$)) {\n    if (typeSchema && typeSchema.fields) {\n      const fieldNames = Object.keys(typeSchema.fields);\n      // If most of the item keys match the schema fields, assume it's this type\n      const matchingFields = fieldNames.filter((field) => field in item);\n      if (\n        matchingFields.length > 0 &&\n        matchingFields.length / fieldNames.length >= 0.5\n      ) {\n        return typeName;\n      }\n    }\n  }\n  return null;\n}\n\ndocument\n  .getElementById(isUpdate ? \"updateForm\" : \"createForm\")\n  .addEventListener(\"submit\", async (e) => {\n    e.preventDefault();\n\n    // Create an object to hold our updated data\n    const updatedData = {};\n\n    // Extract the current form values\n    const formData = new FormData(e.target);\n\n    // Debug log\n    console.log(\"Form data entries:\");\n    for (let [key, value] of formData.entries()) {\n      console.log(`FormData Key: ${key}, Value: ${value}`);\n    }\n\n    // Process each form field\n    for (let [key, value] of formData.entries()) {\n      // Skip disabled fields (they won't be included in FormData anyway)\n      const field = e.target.querySelector(`[name=\"${key}\"]`);\n      if (field && field.disabled) continue;\n\n      // Parse the key to handle nested structures\n      const parts = key.split(/[\\[\\].]/).filter(Boolean);\n\n      // Start at the root of our data object\n      let current = updatedData;\n\n      // Build the nested structure\n      for (let i = 0; i < parts.length; i++) {\n        const part = parts[i];\n\n        if (i === parts.length - 1) {\n          // We're at the leaf node, set the actual value\n\n          // Handle boolean values from checkboxes\n          if (field && field.type === \"checkbox\") {\n            value = field.checked;\n          }\n          // Convert string \"true\"/\"false\" to boolean\n          else if (value === \"true\" || value === \"false\") {\n            value = value === \"true\";\n          }\n          // Convert numeric strings to numbers\n          else if (!isNaN(value) && value !== \"\") {\n            value = Number(value);\n          }\n\n          // Set the value in our data structure\n          current[part] = value;\n        } else {\n          // We're building the nested structure\n\n          // Check if the next part is a number (array index)\n          if (/^\\d+$/.test(parts[i + 1])) {\n            // Create array if it doesn't exist\n            current[part] = current[part] || [];\n          } else {\n            // Create object if it doesn't exist\n            current[part] = current[part] || {};\n          }\n\n          // Move deeper into the structure\n          current = current[part];\n        }\n      }\n    }\n\n    // Process unchecked checkboxes (they don't appear in FormData)\n    const allCheckboxes = e.target.querySelectorAll('input[type=\"checkbox\"]');\n    allCheckboxes.forEach((checkbox) => {\n      if (checkbox.disabled) return; // Skip disabled checkboxes\n\n      const name = checkbox.name;\n      const parts = name.split(/[\\[\\].]/).filter(Boolean);\n\n      // Check if this checkbox's value is already in updatedData\n      // If not, it means it was unchecked\n      let current = updatedData;\n      let exists = true;\n\n      for (let i = 0; i < parts.length - 1; i++) {\n        if (!current[parts[i]]) {\n          exists = false;\n          break;\n        }\n        current = current[parts[i]];\n      }\n\n      const lastPart = parts[parts.length - 1];\n      if (exists && !(lastPart in current)) {\n        current[lastPart] = false;\n      }\n    });\n\n    // Copy over the ID field to ensure it's included\n    if (isUpdate && data && data.id) {\n      updatedData.id = data.id;\n    }\n\n    try {\n      const response = await fetch(window.location.pathname, {\n        method: isUpdate ? \"PUT\" : \"POST\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify(updatedData),\n      });\n\n      if (response.ok) {\n        const result = await response.json();\n        console.log(\"Success:\", result);\n        showToast(\n          \"Success\",\n          `Your data has been ${\n            isUpdate ? \"updated\" : \"submitted\"\n          } successfully!`,\n          \"success\"\n        );\n      } else {\n        const errorText = await response.text();\n        console.error(\"Error:\", response.status, response.statusText);\n        showToast(\"Error\", `Error ${response.status}: ${errorText}`, \"error\");\n      }\n    } catch (error) {\n      console.error(\"Fetch error:\", error);\n      showToast(\n        \"Error\",\n        \"An unexpected error occurred. Please try again.\",\n        \"error\"\n      );\n    }\n  });\n\n// Toast function\nfunction showToast(title, message, type) {\n  const toastContainer =\n    document.getElementById(\"toastContainer\") || createToastContainer();\n  const toast = document.createElement(\"div\");\n  toast.className = `toast ${\n    type === \"success\" ? \"border-green-600\" : \"border-red-600\"\n  } p-4 mb-4 rounded shadow-lg bg-gray-900 text-gray-100 border-l-4 p-4 rounded-lg shadow-md animate-fadeIn`;\n  toast.innerHTML = `\n    <strong class=\"block text-lg\">${title}</strong>\n    <span class=\"block text-sm\">${message}</span>\n  `;\n  toastContainer.appendChild(toast);\n\n  // Automatically remove the toast after 5 seconds\n  setTimeout(() => {\n    toast.remove();\n  }, 5000);\n}\n\nfunction createToastContainer() {\n  const container = document.createElement(\"div\");\n  container.id = \"toastContainer\";\n  container.className = \"fixed top-4 left-4 z-50 space-y-4\";\n  document.body.appendChild(container);\n  return container;\n}\n\ndocument.getElementById(\"deleteButton\").addEventListener(\"click\", async () => {\n  if (!data.id) {\n    console.error(\"No ID found for deletion.\");\n    showToast(\"Error\", \"No ID found for deletion.\", \"error\");\n    return;\n  }\n\n  const confirmDelete = confirm(\"Are you sure you want to delete this record?\");\n  if (!confirmDelete) return;\n\n  try {\n    const response = await fetch(window.location.pathname, {\n      method: \"DELETE\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n    });\n\n    if (response.ok) {\n      console.log(\"Record deleted successfully.\");\n      showToast(\"Success\", \"Record deleted successfully!\", \"success\");\n      const currentUrl = window.location.pathname;\n      const urlParts = currentUrl.split(\"/\").filter((part) => part !== \"\");\n      urlParts.pop();\n      const redirectUrl = \"/\" + urlParts.join(\"/\") + \"/\";\n\n      console.log(\"Redirecting to:\", redirectUrl);\n\n      setTimeout(() => {\n        window.location.href = redirectUrl;\n      }, 2000);\n    } else {\n      const errorText = await response.text();\n      console.error(\"Error:\", response.status, response.statusText);\n      showToast(\"Error\", `Error ${response.status}: ${errorText}`, \"error\");\n    }\n  } catch (error) {\n    console.error(\"Fetch error:\", error);\n    showToast(\n      \"Error\",\n      \"An unexpected error occurred. Please try again.\",\n      \"error\"\n    );\n  }\n});\n\n// Form Update Real-Time Logger and Debugger\n\n// Create a logging container\nfunction createLogContainer() {\n  const existingContainer = document.getElementById(\"formDataLogger\");\n  if (existingContainer) return existingContainer;\n\n  const logContainer = document.createElement(\"div\");\n  logContainer.id = \"formDataLogger\";\n  logContainer.className =\n    \"fixed bottom-4 right-4 w-96 max-h-96 bg-gray-800 border border-gray-600 rounded-lg shadow-lg p-3 overflow-y-auto z-50\";\n  logContainer.style.maxHeight = \"400px\";\n\n  logContainer.innerHTML = `\n    <div class=\"flex justify-between items-center mb-2 pb-2 border-b border-gray-600\">\n      <h3 class=\"text-sm font-bold text-white\">Form Data Logger</h3>\n      <div class=\"flex gap-2\">\n        <button id=\"captureFormState\" class=\"text-xs bg-blue-600 px-2 py-1 rounded hover:bg-blue-700\">Capture State</button>\n        <button id=\"clearLogBtn\" class=\"text-xs bg-gray-700 px-2 py-1 rounded hover:bg-gray-600\">Clear</button>\n        <button id=\"toggleLogBtn\" class=\"text-xs bg-gray-700 px-2 py-1 rounded hover:bg-gray-600\">Hide</button>\n      </div>\n    </div>\n    <div id=\"logEntries\" class=\"space-y-2 text-xs\"></div>\n  `;\n\n  document.body.appendChild(logContainer);\n\n  // Add button event listeners\n  document.getElementById(\"clearLogBtn\").addEventListener(\"click\", () => {\n    document.getElementById(\"logEntries\").innerHTML = \"\";\n  });\n\n  document.getElementById(\"toggleLogBtn\").addEventListener(\"click\", (e) => {\n    const logEntries = document.getElementById(\"logEntries\");\n    if (logEntries.style.display === \"none\") {\n      logEntries.style.display = \"block\";\n      e.target.textContent = \"Hide\";\n    } else {\n      logEntries.style.display = \"none\";\n      e.target.textContent = \"Show\";\n    }\n  });\n\n  document.getElementById(\"captureFormState\").addEventListener(\"click\", () => {\n    const currentState = captureFormState();\n    logMessage(\"Form State Snapshot\", currentState, \"snapshot\");\n    console.log(\"Current Form State:\", currentState);\n  });\n\n  return logContainer;\n}\n\n// Capture current form state (all fields)\nfunction captureFormState() {\n  const form = document.getElementById(isUpdate ? \"updateForm\" : \"createForm\");\n  if (!form) return null;\n\n  const formState = {};\n\n  // Get all input elements\n  const inputs = form.querySelectorAll(\"input, select, textarea\");\n  inputs.forEach((input) => {\n    if (!input.name) return;\n\n    // Skip disabled inputs if we want to capture only enabled fields\n    // if (input.disabled) return;\n\n    if (input.type === \"checkbox\") {\n      formState[input.name] = input.checked;\n    } else {\n      formState[input.name] = input.value;\n    }\n  });\n\n  // Process the form data into a nested structure\n  const structuredData = {};\n\n  Object.entries(formState).forEach(([key, value]) => {\n    // Parse the key to handle nested structures\n    const parts = key.split(/[\\[\\].]/).filter(Boolean);\n\n    // Start at the root of our data object\n    let current = structuredData;\n\n    // Build the nested structure\n    for (let i = 0; i < parts.length; i++) {\n      const part = parts[i];\n\n      if (i === parts.length - 1) {\n        // We're at the leaf node, set the actual value\n\n        // Handle special value conversions\n        if (typeof value === \"string\") {\n          if (value === \"true\" || value === \"false\") {\n            value = value === \"true\";\n          } else if (!isNaN(value) && value !== \"\") {\n            value = Number(value);\n          }\n        }\n\n        // Set the value in our data structure\n        current[part] = value;\n      } else {\n        // We're building the nested structure\n\n        // Check if the next part is a number (array index)\n        if (/^\\d+$/.test(parts[i + 1])) {\n          // Create array if it doesn't exist\n          current[part] = current[part] || [];\n        } else {\n          // Create object if it doesn't exist\n          current[part] = current[part] || {};\n        }\n\n        // Move deeper into the structure\n        current = current[part];\n      }\n    }\n  });\n\n  return structuredData;\n}\n\n// Log a message to the log container\nfunction logMessage(title, data, type = \"info\") {\n  const logEntries = document.getElementById(\"logEntries\");\n  if (!logEntries) return;\n\n  const timestamp = new Date().toLocaleTimeString();\n  const logEntry = document.createElement(\"div\");\n\n  // Different styling based on type\n  if (type === \"error\") {\n    logEntry.className = \"p-2 rounded bg-red-900/50 border-l-4 border-red-600\";\n  } else if (type === \"warning\") {\n    logEntry.className =\n      \"p-2 rounded bg-yellow-900/50 border-l-4 border-yellow-600\";\n  } else if (type === \"success\") {\n    logEntry.className =\n      \"p-2 rounded bg-green-900/50 border-l-4 border-green-600\";\n  } else if (type === \"snapshot\") {\n    logEntry.className =\n      \"p-2 rounded bg-blue-900/50 border-l-4 border-blue-600\";\n  } else {\n    logEntry.className = \"p-2 rounded bg-gray-800 border border-gray-700\";\n  }\n\n  // Format data object to string\n  let dataHtml = \"\";\n  if (data === null || data === undefined) {\n    dataHtml = '<span class=\"text-gray-400\">null or undefined</span>';\n  } else if (typeof data === \"object\") {\n    try {\n      const jsonStr = JSON.stringify(data, null, 2);\n      if (jsonStr === \"{}\") {\n        dataHtml = '<span class=\"text-orange-400\">Empty object {}</span>';\n      } else {\n        dataHtml = `<pre class=\"text-xs mt-1 p-2 bg-gray-900 rounded overflow-x-auto\">${escapeHtml(\n          jsonStr\n        )}</pre>`;\n      }\n    } catch (e) {\n      dataHtml = `<span class=\"text-red-400\">Error stringifying: ${e.message}</span>`;\n    }\n  } else {\n    dataHtml = `<span>${escapeHtml(String(data))}</span>`;\n  }\n\n  logEntry.innerHTML = `\n    <div class=\"flex justify-between\">\n      <span class=\"font-medium text-white\">${escapeHtml(title)}</span>\n      <span class=\"text-gray-400 text-xs\">${timestamp}</span>\n    </div>\n    <div class=\"mt-1\">${dataHtml}</div>\n  `;\n\n  logEntries.prepend(logEntry);\n\n  // Limit the number of log entries\n  const maxEntries = 50;\n  while (logEntries.children.length > maxEntries) {\n    logEntries.removeChild(logEntries.lastChild);\n  }\n}\n\n// Escape HTML to prevent XSS\nfunction escapeHtml(str) {\n  return String(str)\n    .replace(/&/g, \"&amp;\")\n    .replace(/</g, \"&lt;\")\n    .replace(/>/g, \"&gt;\")\n    .replace(/\"/g, \"&quot;\")\n    .replace(/'/g, \"&#039;\");\n}\n\n// Monitor form element changes\nfunction attachFormMonitors() {\n  const form = document.getElementById(isUpdate ? \"updateForm\" : \"createForm\");\n  if (!form) {\n    logMessage(\n      \"Form not found\",\n      `Could not find form with ID: ${isUpdate ? \"updateForm\" : \"createForm\"}`,\n      \"error\"\n    );\n    return;\n  }\n\n  // Monitor input changes\n  form.addEventListener(\"input\", (e) => {\n    if (!e.target.name) return;\n\n    const fieldName = e.target.name;\n    const newValue =\n      e.target.type === \"checkbox\" ? e.target.checked : e.target.value;\n\n    logMessage(`Field Changed: ${fieldName}`, { value: newValue }, \"info\");\n  });\n\n  // Monitor checkbox toggles\n  form.querySelectorAll('input[type=\"checkbox\"]').forEach((checkbox) => {\n    if (checkbox.id && checkbox.id.includes(\"_toggle\")) {\n      checkbox.addEventListener(\"change\", () => {\n        const targetId = checkbox.id.replace(\"_toggle\", \"_content\");\n        logMessage(`Toggle Section: ${targetId}`, {\n          checked: checkbox.checked,\n          visible: checkbox.checked,\n        });\n      });\n    }\n  });\n\n  // Intercept form submission\n  const originalSubmit = form.onsubmit;\n  form.onsubmit = async function (e) {\n    e.preventDefault();\n\n    logMessage(\n      \"Form Submission Started\",\n      {\n        time: new Date().toISOString(),\n        isUpdate: isUpdate || false,\n      },\n      \"info\"\n    );\n\n    // Capture and log the complete form state\n    const formState = captureFormState();\n    logMessage(\n      \"Form Submission Data\",\n      formState,\n      formState && Object.keys(formState).length ? \"success\" : \"warning\"\n    );\n\n    // Collect FormData entries for debugging\n    const formData = new FormData(form);\n    const formDataEntries = {};\n    for (let [key, value] of formData.entries()) {\n      formDataEntries[key] = value;\n    }\n\n    logMessage(\"Raw FormData Entries\", formDataEntries);\n\n    // Check for empty submission\n    if (!formState || Object.keys(formState).length === 0) {\n      logMessage(\n        \"Empty Form Submission\",\n        \"The form data is empty! Check for form field naming issues.\",\n        \"error\"\n      );\n    }\n\n    // Call the original submit handler if it exists\n    if (originalSubmit) {\n      return originalSubmit.call(this, e);\n    }\n  };\n\n  // Override the existing updateData function if it exists\n  if (typeof window.updateData === \"function\") {\n    const originalUpdateData = window.updateData;\n    window.updateData = function (data) {\n      logMessage(\"updateData Called\", data, \"info\");\n      return originalUpdateData.apply(this, arguments);\n    };\n  }\n\n  // Override the populateFormWithExistingData function\n  if (typeof window.populateFormWithExistingData === \"function\") {\n    const originalPopulate = window.populateFormWithExistingData;\n    window.populateFormWithExistingData = function (data) {\n      logMessage(\"Populating Form with Data\", data, data ? \"info\" : \"warning\");\n\n      if (!data || Object.keys(data).length === 0) {\n        logMessage(\n          \"Empty Data for Form Population\",\n          \"This might explain missing form values\",\n          \"warning\"\n        );\n      }\n\n      // Call original function\n      const result = originalPopulate.apply(this, arguments);\n\n      // Capture the form state after population\n      setTimeout(() => {\n        const formState = captureFormState();\n        logMessage(\"Form State After Population\", formState, \"snapshot\");\n\n        // Check if fields are disabled when they shouldn't be\n        const disabledInputs = form.querySelectorAll(\n          \"input:disabled, select:disabled, textarea:disabled\"\n        );\n        if (disabledInputs.length > 0) {\n          const disabledFields = Array.from(disabledInputs)\n            .map((el) => el.name)\n            .filter(Boolean);\n          if (disabledFields.length > 0) {\n            logMessage(\"Found Disabled Fields\", disabledFields, \"warning\");\n          }\n        }\n      }, 100);\n\n      return result;\n    };\n  }\n\n  // Debug the fetch call\n  const originalFetch = window.fetch;\n  window.fetch = function (...args) {\n    const [url, options] = args;\n\n    if (\n      options &&\n      options.method &&\n      (options.method === \"POST\" || options.method === \"PUT\")\n    ) {\n      logMessage(\n        `Fetch Request (${options.method})`,\n        {\n          url: url,\n          method: options.method,\n          bodyData: options.body ? JSON.parse(options.body) : null,\n        },\n        \"info\"\n      );\n    }\n\n    const fetchPromise = originalFetch.apply(this, args);\n\n    fetchPromise\n      .then((response) => {\n        if (!response.ok) {\n          logMessage(\n            \"Fetch Error\",\n            {\n              status: response.status,\n              statusText: response.statusText,\n            },\n            \"error\"\n          );\n        } else {\n          logMessage(\n            \"Fetch Success\",\n            {\n              status: response.status,\n            },\n            \"success\"\n          );\n        }\n      })\n      .catch((error) => {\n        logMessage(\n          \"Fetch Failed\",\n          {\n            error: error.message,\n          },\n          \"error\"\n        );\n      });\n\n    return fetchPromise;\n  };\n}\n\n// Initialize the logger\nfunction initFormLogger() {\n  createLogContainer();\n  attachFormMonitors();\n\n  // Log initial system state\n  logMessage(\n    \"Form Logger Initialized\",\n    {\n      url: window.location.href,\n      isUpdate: typeof isUpdate !== \"undefined\" ? isUpdate : false,\n      time: new Date().toISOString(),\n    },\n    \"info\"\n  );\n\n  // Check if existingData is available\n  if (typeof existingData !== \"undefined\") {\n    logMessage(\n      \"Existing Data Found\",\n      existingData,\n      existingData ? \"info\" : \"warning\"\n    );\n  } else {\n    logMessage(\"No Existing Data\", \"existingData is not defined\", \"warning\");\n  }\n\n  // Log initial schema\n  if (typeof schema !== \"undefined\") {\n    logMessage(\"Schema Structure\", {\n      hasFields: schema.fields ? true : false,\n      nestedTypes: schema.$ ? Object.keys(schema.$) : [],\n    });\n  }\n\n  // Check form action is correctly set\n  setTimeout(() => {\n    const form = document.getElementById(\n      isUpdate ? \"updateForm\" : \"createForm\"\n    );\n    if (form) {\n      logMessage(\"Form Configuration\", {\n        action: form.action || window.location.pathname,\n        method: form.method || \"POST/PUT via JS\",\n        id: form.id,\n        containsInputs:\n          form.querySelectorAll(\"input, select, textarea\").length > 0,\n      });\n    }\n  }, 500);\n}\n\n// Run on page load\ndocument.addEventListener(\"DOMContentLoaded\", function () {\n  setTimeout(initFormLogger, 100); // Slight delay to ensure page is fully loaded\n});\n\n// For immediate execution if the page is already loaded\nif (\n  document.readyState === \"complete\" ||\n  document.readyState === \"interactive\"\n) {\n  setTimeout(initFormLogger, 100);\n}\n"
  },
  {
    "path": "panther/panel/templates/detail.html",
    "content": "{% extends \"base.html\" %} {% block content %}\n<div class=\"container mx-auto px-4 py-8\">\n  <div class=\"flex justify-between items-center mb-8\">\n    <h1 class=\"text-2xl font-bold\">Update Record</h1>\n    <button\n      onclick=\"redirectToTable()\"\n      class=\"px-4 py-2 bg-gray-800 rounded hover:bg-gray-700\"\n    >\n      Back to List\n    </button>\n  </div>\n\n  <form id=\"updateForm\" class=\"mt-6\">\n    <div\n      id=\"dynamicInputs\"\n      class=\"bg-gray-800 border-gray-400 rounded-lg p-6 shadow-lg space-y-4\"\n    ></div>\n    <div class=\"flex justify-start mt-6 space-x-4\">\n      <button\n        type=\"submit\"\n        class=\"px-6 py-2 bg-blue-600 rounded hover:bg-blue-700 text-white\"\n      >\n        Update\n      </button>\n      <button\n        type=\"button\"\n        id=\"deleteButton\"\n        class=\"px-6 py-2 bg-red-600 rounded hover:bg-red-700 text-white\"\n      >\n        Delete\n      </button>\n    </div>\n  </form>\n</div>\n\n<script>\n  const data = JSON.parse(`{{data|tojson|safe}}`);\n  console.log(\"Data passed to the form:\", data); // Debugging log\n  const isUpdate = true;\n  const existingData = data;\n  function redirectToTable() {\n    const currentUrl = window.location.pathname;\n      const urlParts = currentUrl.split(\"/\").filter((part) => part !== \"\");\n      urlParts.pop();\n      const redirectUrl = \"/\" + urlParts.join(\"/\") + \"/\";\n\n      console.log(\"Redirecting to:\", redirectUrl);\n\n      window.location.href = redirectUrl;\n\n  }\n  {% include \"create.js\" %}\n\n</script>\n{% endblock %}\n"
  },
  {
    "path": "panther/panel/templates/home.html",
    "content": "{% extends \"base.html\" %} {% block content %}\n<div class=\"flex gap-4\">\n  {% include \"sidebar.html\" %}\n  <table class=\"w-full bg-gray-800 rounded overflow-hidden\"></table>\n</div>\n<script>\n  {% include \"home.js\" %}\n</script>\n{% endblock %}\n"
  },
  {
    "path": "panther/panel/templates/home.js",
    "content": "function getCurrentTableIndex() {\n  const path = window.location.pathname;\n  const match = path.match(/\\/admin\\/(\\d+)/);\n  return match ? parseInt(match[1]) : 0;\n}\n\nfunction selectTable(element) {\n  const index = element.dataset.index;\n  // Always trigger a page reload even if it's the same index\n  window.location.href = `/admin/${index}/`;\n}\n\nfunction setActiveTableFromUrl() {\n  const currentIndex = getCurrentTableIndex();\n  const tableItems = document.querySelectorAll('.table-item');\n\n  tableItems.forEach((item) => {\n    item.classList.remove('bg-blue-600');\n    item.classList.add('bg-gray-700');\n\n    if (parseInt(item.dataset.index) === currentIndex) {\n      item.classList.remove('bg-gray-700');\n      item.classList.add('bg-blue-600');\n    }\n  });\n}\n\ndocument.addEventListener('DOMContentLoaded', () => {\n  setActiveTableFromUrl();\n});\n"
  },
  {
    "path": "panther/panel/templates/login.html",
    "content": "{% extends \"base.html\" %}\n\n{% block content %}\n  <div class=\"flex items-center justify-center min-h-screen\">\n    <div class=\"bg-gray-800 p-6 rounded-lg shadow-md w-full max-w-sm border border-gray-700\">\n      <h2 class=\"text-xl font-semibold mb-6 text-center\">Login</h2>\n\n      {% if error %}\n        <div class=\"mb-4 p-3 text-sm text-red-600 bg-red-100 border border-red-400 rounded-lg\">\n          {{ error }}\n        </div>\n      {% endif %}\n\n      <form action=\"\" method=\"POST\" class=\"space-y-4\">\n        <!-- Username Field -->\n        <div>\n          <label for=\"username\" class=\"block text-sm font-medium mb-2\">Username:</label>\n          <input\n            type=\"text\"\n            id=\"username\"\n            name=\"username\"\n            required\n            class=\"w-full p-2 bg-gray-700 rounded-lg border border-gray-600 text-white focus:ring-2 focus:ring-blue-500 focus:outline-none\"\n          />\n        </div>\n        <!-- Password Field -->\n        <div>\n          <label for=\"password\" class=\"block text-sm font-medium mb-2\">Password:</label>\n          <input\n            type=\"password\"\n            id=\"password\"\n            name=\"password\"\n            required\n            class=\"w-full p-2 bg-gray-700 rounded-lg border border-gray-600 text-white focus:ring-2 focus:ring-blue-500 focus:outline-none\"\n          />\n        </div>\n        <!-- Submit Button -->\n        <button\n          type=\"submit\"\n          class=\"w-full bg-blue-600 hover:bg-blue-500 text-white py-2 px-4 rounded-lg font-semibold transition-colors\"\n        >\n          Login\n        </button>\n      </form>\n    </div>\n  </div>\n{% endblock %}\n"
  },
  {
    "path": "panther/panel/templates/sidebar.html",
    "content": "\n    <aside class=\"w-64 bg-gray-800 p-4 border-gray-700 shrink-0 rounded\">\n        <h2 class=\"text-lg font-semibold mb-4\">Available Tables</h2>\n        <div class=\"space-y-2\">\n            {% for table in tables %}\n            <div class=\"table-item p-3 rounded-lg bg-gray-700 hover:bg-gray-600 cursor-pointer transition-colors\"\n                data-index=\"{{loop.index0}}\" onclick=\"selectTable(this)\">\n                <div class=\"text-sm font-medium\">{{table.name}}</div>\n                <div class=\"text-xs text-gray-400\">{{table.module}}</div>\n            </div>\n            {% endfor %}\n        </div>\n    </aside>\n"
  },
  {
    "path": "panther/panel/templates/table.html",
    "content": "{% extends \"base.html\" %} {% block content %}\n<div class=\"flex items-start gap-4\">\n  <aside class=\"w-64 bg-gray-800 p-4 border-gray-700 shrink-0 rounded\">\n    <h2 class=\"text-lg font-semibold mb-4\">Available Tables</h2>\n    <div class=\"space-y-2\">\n      {% for table in tables %}\n      <div\n        class=\"table-item p-3 rounded-lg bg-gray-700 hover:bg-gray-600 cursor-pointer transition-colors flex justify-between items-center\"\n        data-index=\"{{loop.index0}}\"\n        onclick=\"selectTable(this, event)\"\n      >\n        <div>\n          <div class=\"text-sm font-medium\">{{table.name}}</div>\n          <div class=\"text-xs text-gray-400\">{{table.module}}</div>\n        </div>\n        <button \n          class=\"add-record-btn hidden p-2 text-xs flex bg-green-600 text-white rounded-full hover:bg-green-700 transition-colors duration-200\"\n          onclick=\"goToCreatePage(event)\"\n          title=\"Create new record\"\n        >\n       Add\n          <svg\n            class=\"w-4 h-4\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            viewBox=\"0 0 24 24\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <path\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              stroke-width=\"2\"\n              d=\"M12 6v6m0 0v6m0-6h6m-6 0H6\"\n            ></path>\n          </svg>\n        </button>\n      </div>\n      {% endfor %}\n    </div>\n    \n  </aside>\n\n  <div>\n    <table class=\"w-full bg-gray-800 rounded overflow-hidden\">\n      <thead class=\"bg-gray-700\" id=\"tableHead\"></thead>\n      <tbody id=\"tableBody\">\n        {% for record in records %}\n        <tr class=\"record-row border-t border-gray-700 hover:bg-gray-750\">\n          <td class=\"record-data hidden\">{{record}}</td>\n        </tr>\n        {% endfor %}\n      </tbody>\n      <tfoot\n        id=\"paginationControls\"\n        class=\"my-4 flex-grow flex mx-[100%] gap-4 justify-center w-full min-w-80\"\n      ></tfoot>\n    </table>\n  </div>\n</div>\n<style>\n  details[open] summary svg {\n    transform: rotate(180deg);\n  }\n\n  details summary::-webkit-details-marker {\n    display: none;\n  }\n</style>\n<script>\n  {% include \"table.js\" %}\n</script>\n\n{% endblock %}\n"
  },
  {
    "path": "panther/panel/templates/table.js",
    "content": "const fields = JSON.parse(`{{fields|tojson|safe}}`);\nconst fieldsObject = fields.fields;\nconst fieldsArray = Object.entries(fieldsObject).map(([key, value]) => ({\n  title: value.title || key,\n  type: value.type || [], \n  required: value.required || false, \n}));\n\nfunction pythonToJSON(str) {\n  str = str.replace(/datetime\\.datetime\\(([^)]+)\\)/g, (match, contents) => {\n    const parts = contents.split(\",\").map((part) => part.trim());\n    if (parts.length >= 6) {\n      return `\"${parts[0]}-${parts[1].padStart(2, \"0\")}-${parts[2].padStart(\n        2,\n        \"0\"\n      )} ${parts[3].padStart(2, \"0\")}:${parts[4].padStart(\n        2,\n        \"0\"\n      )}:${parts[5].padStart(2, \"0\")}\"`;\n    }\n    return '\"Invalid datetime\"';\n  });\n\n  str = str.replace(/tzinfo=\\)/g, \"tzinfo=None)\");\n  return str\n    .replace(/'/g, '\"')\n    .replace(/False/g, \"false\")\n    .replace(/True/g, \"true\")\n    .replace(/None/g, \"null\");\n}\nfunction goToCreatePage() {\n  // Get the current URL without any trailing slash\n  const currentUrl = window.location.href.replace(/\\/+$/, \"\");\n  // Navigate to the current URL + /create\n  window.location.href = `${currentUrl}/create`;\n}\n\nfunction getDataType(value) {\n  if (value === null) return \"null\";\n  if (Array.isArray(value)) return \"array\";\n  return typeof value;\n}\n\nlet allRecords = [];\nfunction initializeRecords() {\n  if (allRecords.length === 0) {\n    try {\n      allRecords = Array.from(document.querySelectorAll(\".record-data\")).map(\n        (item) => {\n          try {\n            return JSON.parse(pythonToJSON(item.textContent.trim()));\n          } catch (e) {\n            console.error(\"Error parsing record:\", e);\n            return {};\n          }\n        }\n      );\n    } catch (e) {\n      console.error(\"Error initializing records:\", e);\n      allRecords = [];\n    }\n  }\n}\n\nfunction formatValue(value, type) {\n  if (value === null) return '<span class=\"text-gray-400\">null</span>';\n\n  switch (type) {\n    case \"array\":\n      return `\n        <div class=\"w-full\">\n          <details class=\"bg-gray-700 rounded-lg max-w-[300px]\">\n            <summary class=\"cursor-pointer px-4 py-2 text-sm font-medium text-gray-300 hover:bg-gray-600 rounded-lg flex items-center justify-between\">\n              <span>Array (${value.length} items)</span>\n              <svg class=\"w-4 h-4 transition-transform duration-200\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"/>\n              </svg>\n            </summary>\n            <div class=\"px-4 py-2 border-t border-gray-600\">\n              ${value\n                .map(\n                  (item, index) => `\n                <div class=\"py-1 text-sm text-gray-300\">\n                  <span class=\"text-gray-400\">[${index}]:</span>\n                  ${formatValue(item, getDataType(item))}\n                </div>\n              `\n                )\n                .join(\"\")}\n            </div>\n          </details>\n        </div>\n      `;\n    case \"object\":\n      if (value === null) {\n        return '<span class=\"text-gray-400\">null</span>';\n      }\n      return `\n        <div class=\"w-full max-w-[300px]\">\n          <details class=\"bg-gray-700 rounded-lg\">\n            <summary class=\"cursor-pointer px-4 py-2 text-sm font-medium text-gray-300 hover:bg-gray-600 rounded-lg flex items-center justify-between\">\n              <span>Object (${Object.keys(value).length} props)</span>\n              <svg class=\"w-4 h-4 transition-transform duration-200\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"/>\n              </svg>\n            </summary>\n            <div class=\"px-4 py-2 border-t border-gray-600\">\n              ${Object.entries(value)\n                .map(\n                  ([key, val]) => `\n                <div class=\"py-1 text-sm text-gray-300\">\n                  <span class=\"text-gray-400\">${key}:</span>\n                  ${formatValue(val, getDataType(val))}\n                </div>\n              `\n                )\n                .join(\"\")}\n            </div>\n          </details>\n        </div>\n      `;\n    case \"boolean\":\n      return `<span class=\"px-2 py-1 rounded ${\n        value ? \"bg-green-500/20 text-green-400\" : \"bg-red-500/20 text-red-400\"\n      }\">${value}</span>`;\n    default:\n      return `<span class=\"text-sm ${\n        typeof value === \"string\" ? \"font-mono\" : \"\"\n      }\">${String(value)}</span>`;\n  }\n}\n\nfunction toggleDropdown(button) {\n  const dropdown = button.nextElementSibling;\n  const allDropdowns = document.querySelectorAll(\n    \".relative.inline-block .absolute\"\n  );\n\n  // Close all other dropdowns\n  allDropdowns.forEach((d) => {\n    if (d !== dropdown) d.classList.add(\"hidden\");\n  });\n\n  // Toggle current dropdown\n  dropdown.classList.toggle(\"hidden\");\n\n  // Close dropdown when clicking outside\n  const closeDropdown = (e) => {\n    if (!button.contains(e.target) && !dropdown.contains(e.target)) {\n      dropdown.classList.add(\"hidden\");\n      document.removeEventListener(\"click\", closeDropdown);\n    }\n  };\n\n  document.addEventListener(\"click\", closeDropdown);\n}\n\n// Define pagination variables\nlet currentPage = 1;\nconst rowsPerPage = 10; // Number of rows per page\n\nfunction renderTable() {\n  // Initialize records if not already done\n  initializeRecords();\n\n  const thead = document.getElementById(\"tableHead\");\n  // Use the global records array instead of re-parsing\n  const records = allRecords;\n  console.log(records);\n  if (records.length > 0) {\n    const firstRecord = records[0];\n    const headers = Object.entries(firstRecord).map(([key, value]) => ({\n      key,\n      type: getDataType(value),\n    }));\n    // Render table headers\n    thead.innerHTML = `\n      <tr>\n        ${headers\n          .map(\n            (field) => `\n          <th class=\"px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider\">\n            ${field.key} (${field.type})\n          </th>\n        `\n          )\n          .join(\"\")}\n      </tr>\n    `;\n\n    // Calculate start and end indices for the current page\n    const startIndex = (currentPage - 1) * rowsPerPage;\n    const endIndex = startIndex + rowsPerPage;\n\n    // Get the rows for the current page\n    const rowsForPage = records.slice(startIndex, endIndex);\n\n    // Get all row elements\n    const rowElements = document.querySelectorAll(\".record-row\");\n\n    // Render table rows\n    rowElements.forEach((row, index) => {\n      const record = rowsForPage[index];\n      if (record) {\n        row.innerHTML = headers\n          .map(({ key }) => {\n            const cellValue = record[key];\n            const cellType = getDataType(cellValue);\n\n            // If this is the ID cell, make it clickable\n            if (key === \"id\") {\n              return `\n              <td class=\"px-6 py-4\">\n                <a href=\"${window.location.href.replace(\n                  /\\/$/,\n                  \"\"\n                )}/${cellValue}\" \n                   class=\"text-blue-400 hover:text-blue-300 hover:underline cursor-pointer\">\n                  ${formatValue(cellValue, cellType)}\n                </a>\n              </td>\n            `;\n            } else {\n              return `\n              <td class=\"px-6 py-4\">\n                ${formatValue(cellValue, cellType)}\n              </td>\n            `;\n            }\n          })\n          .join(\"\");\n        row.style.display = \"\"; // Show the row\n\n        // Remove the row click event\n        row.style.cursor = \"default\";\n        row.onclick = null;\n      } else {\n        row.style.display = \"none\"; // Hide unused rows\n      }\n    });\n\n    // Render pagination controls\n    renderPaginationControls(records.length);\n  }\n}\n\nfunction renderPaginationControls(totalRows) {\n  const paginationContainer = document.getElementById(\"paginationControls\");\n  const totalPages = Math.ceil(totalRows / rowsPerPage);\n\n  // Render \"Previous\" and \"Next\" buttons\n  paginationContainer.innerHTML = `\n    <button \n      class=\"px-4 py-2 bg-gray-600 text-white rounded mr-2\" \n      onclick=\"changePage('prev')\" \n      ${currentPage === 1 ? \"disabled\" : \"\"}>\n      Previous\n    </button>\n    <span class=\"text-gray-300 pt-1\">Page ${currentPage} of ${totalPages}</span>\n    <button \n      class=\"px-4 py-2 bg-gray-600 text-white rounded ml-2\" \n      onclick=\"changePage('next')\" \n      ${currentPage === totalPages ? \"disabled\" : \"\"}>\n      Next\n    </button>\n  `;\n}\n\nfunction changePage(direction) {\n  const totalPages = Math.ceil(allRecords.length / rowsPerPage);\n\n  if (direction === \"prev\" && currentPage > 1) {\n    currentPage--;\n  } else if (direction === \"next\" && currentPage < totalPages) {\n    currentPage++;\n  }\n\n  renderTable();\n}\n\nfunction getCurrentTableIndex() {\n  const path = window.location.pathname;\n  const match = path.match(/\\/admin\\/(\\d+)/);\n  return match ? parseInt(match[1]) : 0;\n}\n\nfunction selectTable(element, event) {\n  const index = element.dataset.index;\n  \n  // Hide all add buttons first\n  document.querySelectorAll('.add-record-btn').forEach(btn => {\n    btn.classList.add('hidden');\n  });\n  \n  // Show the add button for the clicked table\n  const addButton = element.querySelector('.add-record-btn');\n  if (addButton) {\n    addButton.classList.remove('hidden');\n  }\n  \n  // Only update URL if it's different from current\n  if (index !== getCurrentTableIndex().toString()) {\n    window.location.href = `/admin/${index}`;\n  }\n}\n\n// Set active table based on URL\nfunction setActiveTableFromUrl() {\n  const currentIndex = getCurrentTableIndex();\n  const tableItems = document.querySelectorAll(\".table-item\");\n\n  tableItems.forEach((item) => {\n    // Hide all add buttons first\n    const addButton = item.querySelector('.add-record-btn');\n    if (addButton) {\n      addButton.classList.add('hidden');\n    }\n    \n    item.classList.remove(\"bg-blue-800\");\n    item.classList.add(\"bg-gray-700\");\n\n    if (parseInt(item.dataset.index) === currentIndex) {\n      item.classList.remove(\"bg-gray-700\");\n      item.classList.add(\"bg-blue-800\");\n      \n      // Show add button for the active table\n      if (addButton) {\n        addButton.classList.remove('hidden');\n      }\n    }\n  });\n}\n\n// Initialize based on URL\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n  initializeRecords();\n  renderTable();\n  setActiveTableFromUrl();\n});\n"
  },
  {
    "path": "panther/panel/urls.py",
    "content": "from panther.panel.views import CreateView, DetailView, HomeView, LoginView, TableView\n\nurl_routing = {\n    '': HomeView,\n    '<index>/': TableView,\n    '<index>/create/': CreateView,\n    'login/': LoginView,\n    '<index>/<document_id>/': DetailView,\n}\n"
  },
  {
    "path": "panther/panel/utils.py",
    "content": "from collections import defaultdict\nfrom types import NoneType\nfrom typing import Any\n\nfrom pydantic import BaseModel\n\nfrom panther.configs import config\nfrom panther.response import IterableDataTypes\n\n\ndef _ref_name(ref: str) -> str:\n    obj_name = ref.rsplit('/', maxsplit=1)[1]\n    return f'${obj_name}'\n\n\ndef clean_model_schema(schema: dict) -> dict:\n    \"\"\"\n    Example:\n        {\n        'title': 'Author',\n        '$': {\n            'Book': {\n                'title': 'Book',\n                'fields': {\n                    'title': {'title': 'Title', 'type': ['string'], 'required': True},\n                    'pages_count': {'title': 'Pages Count', 'type': ['integer'], 'required': True},\n                    'readers': {'title': 'Readers', 'type': ['array', 'null'], 'items': '$Person', 'default': None, 'required': False},\n                    'co_owner': {'type': ['$Person', 'null'], 'default': None, 'required': False}\n                }\n            },\n            'Parent': {\n                'title': 'Parent',\n                'fields': {\n                    'name': {'title': 'Name', 'type': ['string'], 'required': True},\n                    'age': {'title': 'Age', 'type': ['string'], 'required': True},\n                    'has_child': {'title': 'Has Child', 'type': ['boolean'], 'required': True}\n                }\n            },\n            'Person': {\n                'title': 'Person',\n                'fields': {\n                    'age': {'title': 'Age', 'type': ['integer'], 'required': True},\n                    'real_name': {'title': 'Real Name', 'type': ['string'], 'required': True},\n                    'parent': {'type': '$Parent', 'required': True},\n                    'is_alive': {'title': 'Is Alive', 'type': ['boolean'], 'required': True},\n                    'friends': {'title': 'Friends', 'type': ['array'], 'items': '$Person', 'required': True}\n                }\n            }\n        },\n        'fields': {\n            '_id': {'title': ' Id', 'type': ['string', 'null'], 'default': None, 'required': False},\n            'name': {'title': 'Name', 'type': ['string'], 'required': True},\n            'person': {'type': ['$Person', 'null'], 'default': None, 'required': False},\n            'books': {'title': 'Books', 'type': ['array'], 'items': '$Book', 'required': True},\n            'is_male': {'title': 'Is Male', 'type': ['boolean', 'null'], 'required': True}\n        }\n    }\n\n    \"\"\"\n\n    result = defaultdict(dict)\n    result['title'] = schema['title']\n    if '$defs' in schema:\n        for sk, sv in schema['$defs'].items():\n            result['$'][sk] = clean_model_schema(sv)\n\n    for k, v in schema['properties'].items():\n        result['fields'][k] = {}\n        if 'title' in v:\n            result['fields'][k]['title'] = v['title']\n\n        if 'type' in v:\n            result['fields'][k]['type'] = [v['type']]\n\n        if 'anyOf' in v:\n            result['fields'][k]['type'] = [i['type'] if 'type' in i else _ref_name(i['$ref']) for i in v['anyOf']]\n            if 'array' in result['fields'][k]['type']:\n                # One of them was array, so add the `items` field\n                for t in v['anyOf']:\n                    if 'items' in t:\n                        result['fields'][k]['items'] = _ref_name(t['items']['$ref'])\n\n        if 'default' in v:\n            result['fields'][k]['default'] = v['default']\n\n        if '$ref' in v:  # For obj\n            result['fields'][k]['type'] = _ref_name(v['$ref'])\n\n        if 'items' in v:  # For array\n            result['fields'][k]['items'] = _ref_name(v['items']['$ref'])\n\n        result['fields'][k]['required'] = k in schema.get('required', [])\n\n    # Cast it to have a more clear stdout\n    return dict(result)\n\n\ndef get_models():\n    return [\n        {\n            'index': i,\n            'name': model.__name__,\n            'module': model.__module__,\n        }\n        for i, model in enumerate(config.MODELS)\n    ]\n\n\ndef serialize_models(data: Any):\n    if issubclass(type(data), BaseModel):\n        return data.model_dump()\n\n    elif isinstance(data, IterableDataTypes):\n        return [serialize_models(d) for d in data]\n"
  },
  {
    "path": "panther/panel/views.py",
    "content": "import logging\n\nfrom panther import status\nfrom panther.app import GenericAPI\nfrom panther.configs import config\nfrom panther.db.models import BaseUser\nfrom panther.panel.authentications import AdminCookieJWTAuthentication\nfrom panther.panel.middlewares import RedirectToSlashMiddleware\nfrom panther.panel.permissions import IsAuthenticated\nfrom panther.panel.utils import clean_model_schema, get_models, serialize_models\nfrom panther.request import Request\nfrom panther.response import Cookie, RedirectResponse, Response, TemplateResponse\n\nlogger = logging.getLogger('panther')\n\n\nclass LoginView(GenericAPI):\n    middlewares = [RedirectToSlashMiddleware]\n\n    def get(self, request: Request):\n        return TemplateResponse(name='login.html')\n\n    async def post(self, request: Request):\n        from panther.authentications import JWTAuthentication\n\n        user: BaseUser = await config.USER_MODEL.find_one({config.USER_MODEL.USERNAME_FIELD: request.data['username']})\n        if user is None:\n            logger.debug('User not found.')\n            return TemplateResponse(\n                name='login.html',\n                status_code=status.HTTP_400_BAD_REQUEST,\n                context={'error': 'Authentication Error'},\n            )\n        if user.check_password(password=request.data['password']) is False:\n            logger.debug('Password is incorrect.')\n            return TemplateResponse(\n                name='login.html',\n                status_code=status.HTTP_400_BAD_REQUEST,\n                context={'error': 'Authentication Error'},\n            )\n        tokens = await JWTAuthentication.login(user=user)\n        return RedirectResponse(\n            url=request.query_params.get('redirect_to', '..'),\n            status_code=status.HTTP_302_FOUND,\n            set_cookies=[\n                Cookie(key='access_token', value=tokens['access_token'], max_age=config.JWT_CONFIG.life_time),\n                Cookie(key='refresh_token', value=tokens['refresh_token'], max_age=config.JWT_CONFIG.refresh_life_time),\n            ],\n        )\n\n\nclass HomeView(GenericAPI):\n    auth = AdminCookieJWTAuthentication\n    permissions = IsAuthenticated\n\n    def get(self):\n        return TemplateResponse(name='home.html', context={'tables': get_models()})\n\n\nclass TableView(GenericAPI):\n    auth = AdminCookieJWTAuthentication\n    permissions = IsAuthenticated\n    middlewares = [RedirectToSlashMiddleware]\n\n    async def get(self, request: Request, index: int):\n        model = config.MODELS[index]\n        if data := await model.find():\n            data = data\n        else:\n            data = []\n\n        return TemplateResponse(\n            name='table.html',\n            context={\n                'fields': clean_model_schema(model.model_json_schema()),\n                'tables': get_models(),\n                'records': serialize_models(data),\n            },\n        )\n\n\nclass CreateView(GenericAPI):\n    auth = AdminCookieJWTAuthentication\n    permissions = IsAuthenticated\n    middlewares = [RedirectToSlashMiddleware]\n\n    async def get(self, request: Request, index: int):\n        model = config.MODELS[index]\n        return TemplateResponse(\n            name='create.html',\n            context={\n                'fields': clean_model_schema(model.model_json_schema()),\n                'tables': get_models(),\n            },\n        )\n\n    async def post(self, request: Request, index: int):\n        model = config.MODELS[index]\n        request.validate_data(model=model)\n        instance = await model.insert_one(request.validated_data.model_dump())\n        if issubclass(model, BaseUser):\n            await instance.set_password(password=instance.password)\n        return instance\n\n\nclass DetailView(GenericAPI):\n    auth = AdminCookieJWTAuthentication\n    permissions = IsAuthenticated\n    middlewares = [RedirectToSlashMiddleware]\n\n    async def get(self, index: int, document_id: str):\n        model = config.MODELS[index]\n        obj = await model.find_one_or_raise(id=document_id)\n        return TemplateResponse(\n            name='detail.html',\n            context={'fields': clean_model_schema(model.model_json_schema()), 'data': obj.model_dump()},\n        )\n\n    async def put(self, request: Request, index: int, document_id: str):\n        model = config.MODELS[index]\n        request.validate_data(model=model)\n        await model.update_one({'id': document_id}, request.validated_data.model_dump())\n        return await model.find_one(id=document_id)\n\n    async def delete(self, index: int, document_id: str):\n        model = config.MODELS[index]\n        await model.delete_one(id=document_id)\n        return Response(status_code=status.HTTP_204_NO_CONTENT)\n"
  },
  {
    "path": "panther/permissions.py",
    "content": "from abc import abstractmethod\n\nfrom panther.request import Request\nfrom panther.websocket import Websocket\n\n\nclass BasePermission:\n    @abstractmethod\n    async def __call__(self, request: Request | Websocket) -> bool:\n        return True\n\n\nclass IsAuthenticated(BasePermission):\n    async def __call__(self, request: Request) -> bool:\n        return bool(request.user)\n\n\nclass IsAuthenticatedOrReadonly(BasePermission):\n    async def __call__(self, request: Request) -> bool:\n        return request.user or request.method == 'GET'\n"
  },
  {
    "path": "panther/request.py",
    "content": "import logging\nfrom collections.abc import Callable\nfrom typing import Literal\nfrom urllib.parse import parse_qsl\n\nimport orjson as json\nfrom orjson import JSONDecodeError\nfrom pydantic import ValidationError\n\nfrom panther._utils import read_multipart_form_data\nfrom panther.base_request import BaseRequest\nfrom panther.exceptions import BadRequestAPIError, UnprocessableEntityError\n\nlogger = logging.getLogger('panther')\n\n\nclass Request(BaseRequest):\n    def __init__(self, scope: dict, receive: Callable, send: Callable):\n        self._data = ...\n        self.validated_data = None  # It's been set in self.validate_input()\n        super().__init__(scope=scope, receive=receive, send=send)\n\n    @property\n    def method(self) -> Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE']:\n        return self.scope['method']\n\n    @property\n    def data(self) -> dict | bytes:\n        \"\"\"Data before validation\"\"\"\n        if self._data is ...:\n            match (self.headers.content_type or '').split('; boundary='):\n                case ['' | 'application/json']:\n                    self._data = json.loads(self.__body or b'{}')\n                case ['application/x-www-form-urlencoded']:\n                    self._data = {k.decode(): v.decode() for k, v in parse_qsl(self.__body)}\n                case ['multipart/form-data', boundary]:\n                    self._data = read_multipart_form_data(boundary=boundary, body=self.__body)\n                case [unknown]:\n                    # We don't know the `content-type` so just pass the payload to user\n                    logger.warning(f\"'{unknown}' Content-Type is not supported\")\n                    self._data = self.__body\n        return self._data\n\n    async def read_body(self) -> None:\n        \"\"\"Read the entire body from an incoming ASGI message.\"\"\"\n        self.__body = b''\n        more_body = True\n        while more_body:\n            message = await self.asgi_receive()\n            self.__body += message.get('body', b'')\n            more_body = message.get('more_body', False)\n\n    def validate_data(self, model):\n        if isinstance(self.data, bytes):\n            raise UnprocessableEntityError(detail='Content-Type is not valid')\n        if self.data is None:\n            raise BadRequestAPIError(detail='Request body is required')\n        try:\n            # `request` will be ignored in regular `BaseModel`\n            self.validated_data = model(**self.data)\n        except ValidationError as validation_error:\n            error = {'.'.join(str(loc) for loc in e['loc']): e['msg'] for e in validation_error.errors()}\n            raise BadRequestAPIError(detail=error)\n        except JSONDecodeError:\n            raise UnprocessableEntityError(detail='JSON Decode Error')\n"
  },
  {
    "path": "panther/response.py",
    "content": "import asyncio\nimport logging\nfrom collections.abc import AsyncGenerator, Generator\nfrom dataclasses import dataclass\nfrom http import cookies\nfrom sys import version_info\nfrom types import NoneType\nfrom typing import Any, Literal\n\nimport jinja2\n\nfrom panther.exceptions import APIError\n\nif version_info >= (3, 11):\n    from typing import LiteralString\nelse:\n    from typing import TypeVar\n\n    LiteralString = TypeVar('LiteralString')\n\nimport orjson as json\nfrom pantherdb import Cursor as PantherDBCursor\nfrom pydantic import BaseModel\n\nfrom panther import status\nfrom panther._utils import detect_mime_type, to_async_generator\nfrom panther.configs import config\nfrom panther.db.cursor import Cursor\nfrom panther.pagination import Pagination\n\nResponseDataTypes = (\n    list | tuple | set | Cursor | PantherDBCursor | dict | int | float | str | bool | bytes | NoneType | type[BaseModel]\n)\nIterableDataTypes = list | tuple | set | Cursor | PantherDBCursor\nStreamingDataTypes = Generator | AsyncGenerator\n\nlogger = logging.getLogger('panther')\n\n\n@dataclass(slots=True)\nclass Cookie:\n    \"\"\"\n    path: [Optional] Indicates the path that must exist in the requested URL for the browser to send the Cookie header.\n        Default is `/`\n    domain: [Optional] Defines the host to which the cookie will be sent.\n        Default is the host of the current document URL, not including subdomains.\n    max_age: [Optional] Indicates the number of seconds until the cookie expires.\n        A zero or negative number will expire the cookie immediately.\n    secure: [Optional] Indicates that the cookie is sent to the server\n        only when a request is made with the https: scheme (except on localhost)\n    httponly: [Optional] Forbids JavaScript from accessing the cookie,\n        for example, through the `Document.cookie` property.\n    samesite: [Optional] Controls whether a cookie is sent with cross-site requests or not,\n        `lax` is the default behavior if not specified.\n    expires: [Deprecated] In HTTP version 1.1, `expires` was deprecated and replaced with the easier-to-use `max-age`\n    \"\"\"\n\n    key: str\n    value: str\n    domain: str = None\n    max_age: int = None\n    secure: bool = False\n    httponly: bool = False\n    samesite: Literal['none', 'lax', 'strict'] = 'lax'\n    path: str = '/'\n\n\nclass Response:\n    \"\"\"\n    Usage Example:\n        from panther.response import Response\n\n        def my_api():\n            data = {\"message\": \"Hello, World!\"}\n            return Response(data=data)\n    \"\"\"\n\n    content_type = 'application/json'\n\n    def __init__(\n        self,\n        data: ResponseDataTypes = None,\n        status_code: int = status.HTTP_200_OK,\n        headers: dict | None = None,\n        pagination: Pagination | None = None,\n        set_cookies: Cookie | list[Cookie] | None = None,\n    ):\n        \"\"\"\n        :param data: should be an instance of ResponseDataTypes\n        :param status_code: should be int\n        :param headers: should be dict of headers\n        :param pagination: an instance of Pagination or None\n            The `pagination.template()` method will be used\n        :param set_cookies: single cookie or list of cookies you want to set on the client.\n            Set the `max-age` to `0` if you want to delete a cookie.\n        \"\"\"\n        if isinstance(data, (Cursor, PantherDBCursor)):\n            data = list(data)\n        self.data = data\n        self.status_code = status_code\n        self.headers = {'Content-Type': self.content_type} | (headers or {})\n        self.pagination: Pagination | None = pagination\n        self.cookies = None\n        if set_cookies:\n            c = cookies.SimpleCookie()\n            if not isinstance(set_cookies, list):\n                set_cookies = [set_cookies]\n            for cookie in set_cookies:\n                c[cookie.key] = cookie.value\n                c[cookie.key]['path'] = cookie.path\n                c[cookie.key]['secure'] = cookie.secure\n                c[cookie.key]['httponly'] = cookie.httponly\n                c[cookie.key]['samesite'] = cookie.samesite\n                if cookie.domain is not None:\n                    c[cookie.key]['domain'] = cookie.domain\n                if cookie.max_age is not None:\n                    c[cookie.key]['max-age'] = cookie.max_age\n            self.cookies = [(b'Set-Cookie', cookie.OutputString().encode()) for cookie in c.values()]\n\n    def __str__(self):\n        if len(data := str(self.data)) > 30:\n            data = f'{data:.27}...'\n        return f'Response(status_code={self.status_code}, data={data})'\n\n    __repr__ = __str__\n\n    @property\n    def body(self) -> bytes:\n        def default(obj: Any):\n            if isinstance(obj, BaseModel):\n                return obj.model_dump()\n            if isinstance(obj, (Cursor, PantherDBCursor)):\n                return list(obj)\n            if isinstance(obj, bytes):\n                return f'raw bytes is not JSON serializable ({len(obj)} bytes)'\n            raise TypeError(f'Type {type(obj)} not serializable')\n\n        if isinstance(self.data, bytes):\n            return self.data\n        if self.data is None:\n            return b''\n        return json.dumps(self.data, default=default)\n\n    @property\n    def bytes_headers(self) -> list[tuple[bytes, bytes]]:\n        headers = {'Content-Length': len(self.body)} | self.headers\n        result = [(k.encode(), str(v).encode()) for k, v in headers.items()]\n        if self.cookies:\n            result += self.cookies\n        return result\n\n    async def send(self, send, receive):\n        await send({'type': 'http.response.start', 'status': self.status_code, 'headers': self.bytes_headers})\n        await send({'type': 'http.response.body', 'body': self.body, 'more_body': False})\n\n    async def serialize_output(self, output_model: type[BaseModel]):\n        \"\"\"Serializes response data using the given output_model.\"\"\"\n\n        async def handle_output(obj):\n            output = output_model(**obj) if isinstance(obj, dict) else output_model(**obj.model_dump())\n            if hasattr(output_model, 'to_response'):\n                return await output.to_response(instance=obj, data=output.model_dump())\n            return output.model_dump()\n\n        if isinstance(self.data, dict) or isinstance(self.data, BaseModel):\n            self.data = await handle_output(self.data)\n\n        elif isinstance(self.data, IterableDataTypes):\n            results = []\n            for d in self.data:\n                if isinstance(d, dict) or isinstance(d, BaseModel):\n                    results.append(await handle_output(d))\n                else:\n                    msg = 'Type of Response data is not match with `output_model`.\\n*hint: You may want to remove `output_model`'\n                    raise TypeError(msg)\n            self.data = results\n\n        else:\n            msg = (\n                'Type of Response data is not match with `output_model`.\\n*hint: You may want to remove `output_model`'\n            )\n            raise TypeError(msg)\n\n\nclass StreamingResponse(Response):\n    \"\"\"\n    Usage Example:\n        from panther.response import StreamingResponse\n        import time\n\n        def my_generator():\n            for i in range(5):\n                time.sleep(1)\n                yield f\"Chunk {i}\\n\"\n\n        def my_api():\n            return StreamingResponse(data=my_generator())\n    \"\"\"\n\n    content_type = 'application/octet-stream'\n\n    def __init__(self, *args, **kwargs):\n        self.connection_closed = False\n        super().__init__(*args, **kwargs)\n\n    async def listen_to_disconnection(self, receive):\n        message = await receive()\n        if message['type'] == 'http.disconnect':\n            self.connection_closed = True\n\n    @property\n    def bytes_headers(self) -> list[tuple[bytes, bytes]]:\n        result = [(k.encode(), str(v).encode()) for k, v in self.headers.items()]\n        if self.cookies:\n            result += self.cookies\n        return result\n\n    @property\n    async def body(self) -> AsyncGenerator:\n        if not isinstance(self.data, (Generator, AsyncGenerator)):\n            raise TypeError(f'Type {type(self.data)} is not streamable, should be `Generator` or `AsyncGenerator`.')\n\n        if isinstance(self.data, Generator):\n            self.data = to_async_generator(self.data)\n\n        async for chunk in self.data:\n            if isinstance(chunk, bytes):\n                yield chunk\n            elif chunk is None:\n                yield b''\n            else:\n                yield json.dumps(chunk)\n\n    async def send(self, send, receive):\n        # Send Headers\n        await send({'type': 'http.response.start', 'status': self.status_code, 'headers': self.bytes_headers})\n        # Send Body as chunks\n        asyncio.create_task(self.listen_to_disconnection(receive=receive))\n        async for chunk in self.body:\n            if self.connection_closed:\n                break\n            await send({'type': 'http.response.body', 'body': chunk, 'more_body': True})\n        else:\n            await send({'type': 'http.response.body', 'body': b'', 'more_body': False})\n\n\nclass HTMLResponse(Response):\n    \"\"\"\n    Usage Example:\n        from panther.response import HTMLResponse\n\n        def my_api():\n            html = \"<h1>Hello, World!</h1>\"\n            return HTMLResponse(data=html)\n    \"\"\"\n\n    content_type = 'text/html; charset=utf-8'\n\n    @property\n    def body(self) -> bytes:\n        if isinstance(self.data, bytes):\n            return self.data\n        return self.data.encode()\n\n\nclass PlainTextResponse(Response):\n    \"\"\"\n    Usage Example:\n        from panther.response import PlainTextResponse\n\n        def my_api():\n            return PlainTextResponse(data=\"Hello, World!\")\n    \"\"\"\n\n    content_type = 'text/plain; charset=utf-8'\n\n    @property\n    def body(self) -> bytes:\n        if isinstance(self.data, bytes):\n            return self.data\n        return self.data.encode()\n\n\nclass FileResponse(Response):\n    \"\"\"\n    Usage Example:\n        from panther.response import FileResponse\n\n        def my_api():\n            return FileResponse(data=\"file.txt\")\n    \"\"\"\n\n    def __init__(self, file_path: str, headers: dict | NoneType = None, status_code: int = status.HTTP_200_OK):\n        \"\"\"\n        :param file_path: path of the file\n        :param headers: should be dict of headers\n        :param status_code: should be int\n        \"\"\"\n        file = config.BASE_DIR / file_path\n        if not file.exists():\n            super().__init__(data={'detail': 'Not Found'}, headers=headers, status_code=status.HTTP_404_NOT_FOUND)\n        else:\n            headers = {'Content-Type': detect_mime_type(file)} | (headers or {})\n            super().__init__(data=file.read_bytes(), headers=headers, status_code=status_code)\n\n\nclass TemplateResponse(HTMLResponse):\n    \"\"\"\n    Usage Example:\n        from panther.response import TemplateResponse\n\n        def my_api():\n            context = {\"name\": \"Ali\"}\n            return TemplateResponse(name=\"index.html\", context=context)\n    \"\"\"\n\n    \"\"\"\n    You may want to declare `TEMPLATES_DIR` in your configs, default is '.'\n\n    Example:\n        TEMPLATES_DIR = 'templates/'\n    \"\"\"\n\n    def __init__(\n        self,\n        source: str | LiteralString | NoneType = None,\n        name: str | NoneType = None,\n        context: dict | NoneType = None,\n        headers: dict | NoneType = None,\n        status_code: int = status.HTTP_200_OK,\n    ):\n        \"\"\"\n        :param source: should be a string\n        :param name: name of the template file (should be with its extension, e.g. index.html)\n        :param context: should be dict of items\n        :param headers: should be dict of headers\n        :param status_code: should be int\n        \"\"\"\n        if name:\n            try:\n                template = config.JINJA_ENVIRONMENT.get_template(name=name)\n            except jinja2.exceptions.TemplateNotFound:\n                loaded_path = ' - '.join(\n                    ' - '.join(loader.searchpath)\n                    for loader in config.JINJA_ENVIRONMENT.loader.loaders\n                    if isinstance(loader, jinja2.loaders.FileSystemLoader)\n                )\n                error = (\n                    f'`{name}` Template Not Found.\\n'\n                    f'* Make sure `TEMPLATES_DIR` in your configs is correct, Current is {loaded_path}'\n                )\n                logger.error(error)\n                raise APIError\n        else:\n            template = config.JINJA_ENVIRONMENT.from_string(source=source)\n        super().__init__(\n            data=template.render(context or {}),\n            headers=headers,\n            status_code=status_code,\n        )\n\n\nclass RedirectResponse(Response):\n    \"\"\"\n    Usage Example:\n        from panther.response import RedirectResponse\n\n        def my_api():\n            return RedirectResponse(url=\"/new-location\")\n    \"\"\"\n\n    def __init__(\n        self,\n        url: str,\n        headers: dict | None = None,\n        status_code: int = status.HTTP_307_TEMPORARY_REDIRECT,\n        set_cookies: list[Cookie] | None = None,\n    ):\n        headers = headers or {}\n        headers['Location'] = url\n        super().__init__(\n            headers=headers,\n            status_code=status_code,\n            set_cookies=set_cookies,\n        )\n"
  },
  {
    "path": "panther/routings.py",
    "content": "import re\nimport types\nfrom collections import Counter\nfrom collections.abc import Callable, Mapping, MutableMapping\nfrom copy import deepcopy\nfrom functools import partial, reduce\n\nfrom panther.configs import config\nfrom panther.exceptions import PantherError\n\n\ndef flatten_urls(urls: dict) -> dict:\n    return dict(_flattening_urls(urls))\n\n\ndef _flattening_urls(data: dict | Callable, url: str = ''):\n    # Add `/` add the end of url\n    if not url.endswith('/'):\n        url = f'{url}/'\n\n    if isinstance(data, dict):\n        if data == {}:\n            # User didn't define any endpoint,\n            #   So we just reserve this path so won't be used in path variables.\n            yield url.removeprefix('/'), {}\n\n        for k, v in data.items():\n            yield from _flattening_urls(v, f'{url}{k}')\n    else:\n        # Remove `/` prefix of url\n        url = url.removeprefix('/')\n\n        # Collect it, if it doesn't have problem\n        _is_url_endpoint_valid(url=url, endpoint=data)\n        yield url, data\n\n\ndef _is_url_endpoint_valid(url: str, endpoint: Callable):\n    if endpoint is ...:\n        raise PantherError(f\"URL Can't Point To Ellipsis. ('{url}' -> ...)\")\n    if endpoint is None:\n        raise PantherError(f\"URL Can't Point To None. ('{url}' -> None)\")\n    if url and not re.match(r'^[a-zA-Z<>0-9_/-]+$', url):\n        raise PantherError(f\"URL Is Not Valid. --> '{url}'\")\n    elif isinstance(endpoint, types.ModuleType):\n        raise PantherError(f\"URL Can't Point To Module. --> '{url}'\")\n\n\ndef finalize_urls(urls: dict) -> dict:\n    \"\"\"Convert flat dict to nested\"\"\"\n    urls_list = []\n    for url, endpoint in urls.items():\n        path = {}\n        if url == '':\n            # This condition only happen when\n            #   user defines the root url == '' instead of '/'\n            url = '/'\n\n        for single_path in list(filter(lambda x: x != '', url.split('/')[:-1][::-1])) or ['']:\n            if single_path != '' and not path:\n                path = {single_path: {'': endpoint}}\n            else:\n                path = {single_path: path or endpoint}\n        urls_list.append(path)\n    final_urls = _merge(*urls_list) if urls_list else {}\n    check_urls_path_variables(final_urls)\n    return final_urls\n\n\ndef check_urls_path_variables(urls: dict, path: str = '') -> None:\n    middle_route_error = []\n    last_route_error = []\n    for key, value in urls.items():\n        new_path = f'{path}/{key}'\n\n        if isinstance(value, dict):\n            if key.startswith('<'):\n                middle_route_error.append(new_path)\n            check_urls_path_variables(value, path=new_path)\n        elif key.startswith('<'):\n            last_route_error.append(new_path)\n\n    if len(middle_route_error) > 1:\n        msg = '\\n\\t- ' + '\\n\\t- '.join(e for e in middle_route_error)\n        raise PantherError(f\"URLs can't have same-level path variables that point to a dict: {msg}\")\n\n    if len(last_route_error) > 1:\n        msg = '\\n\\t- ' + '\\n\\t- '.join(e for e in last_route_error)\n        raise PantherError(f\"URLs can't have same-level path variables that point to an endpoint: {msg}\")\n\n\ndef _merge(destination: MutableMapping, *sources) -> MutableMapping:\n    return _simplify_urls(reduce(partial(_deepmerge), sources, destination))\n\n\ndef _simplify_urls(urls):\n    simplified_urls = {}\n\n    for key, value in urls.items():\n        if isinstance(value, dict):\n            simplified_value = _simplify_urls(value)\n            if isinstance(simplified_value, dict) and len(simplified_value) == 1 and '' in simplified_value:\n                simplified_urls[key] = simplified_value['']\n            else:\n                simplified_urls[key] = simplified_value\n        else:\n            simplified_urls[key] = value\n\n    return simplified_urls\n\n\ndef _deepmerge(dst, src):\n    \"\"\"Credit to Travis Clarke --> https://github.com/clarketm/mergedeep\"\"\"\n    for key in src:\n        if key in dst:\n            if _is_recursive_merge(dst[key], src[key]):\n                _deepmerge(dst[key], src[key])\n            else:\n                dst[key] = deepcopy(src[key])\n        else:\n            dst[key] = deepcopy(src[key])\n    return dst\n\n\ndef _is_recursive_merge(a, b):\n    both_mapping = isinstance(a, Mapping) and isinstance(b, Mapping)\n    both_counter = isinstance(a, Counter) and isinstance(b, Counter)\n    return both_mapping and not both_counter\n\n\nENDPOINT_NOT_FOUND = (None, '')\n\n\ndef find_endpoint(path: str) -> tuple[Callable | None, str]:\n    urls = config.URLS\n\n    # 'user/list/?name=ali' --> 'user/list/' --> 'user/list' --> ['user', 'list']\n    parts = path.split('?')[0].strip('/').split('/')\n    paths_len = len(parts)\n\n    found_path = []\n    for i, part in enumerate(parts):\n        last_path = bool((i + 1) == paths_len)\n        found = urls.get(part)\n\n        if last_path:\n            # `found` is callable\n            if callable(found):\n                found_path.append(part)\n                return found, '/'.join(found_path)\n\n            # `found` is dict\n            if isinstance(found, dict):\n                if (endpoint := found.get('')) and callable(endpoint):\n                    found_path.append(part)\n                    return endpoint, '/'.join(found_path)\n                else:\n                    return ENDPOINT_NOT_FOUND\n\n            # `found` is None\n            for key, value in urls.items():\n                if key.startswith('<'):\n                    if callable(value):\n                        found_path.append(key)\n                        return value, '/'.join(found_path)\n\n                    elif isinstance(value, dict) and (endpoint := value.get('')):\n                        if callable(endpoint):\n                            found_path.append(key)\n                            return endpoint, '/'.join(found_path)\n                        else:\n                            return ENDPOINT_NOT_FOUND\n\n            return ENDPOINT_NOT_FOUND\n\n        # `found` is dict\n        elif isinstance(found, dict):\n            found_path.append(part)\n            urls = found\n            continue\n\n        # `found` is callable\n        elif callable(found):\n            return ENDPOINT_NOT_FOUND\n\n        else:\n            # `found` is None\n            for key, value in urls.items():\n                if key.startswith('<') and isinstance(value, dict):\n                    found_path.append(key)\n                    urls = value\n                    break\n            else:\n                return ENDPOINT_NOT_FOUND\n"
  },
  {
    "path": "panther/serializer.py",
    "content": "import datetime\nimport typing\nfrom sys import version_info\nfrom typing import Any\n\nimport pydantic\nimport pydantic.fields\nimport pydantic_core._pydantic_core\n\nfrom panther.db import Model\nfrom panther.utils import run_coroutine, timezone_now\n\n\nclass MetaModelSerializer:\n    KNOWN_CONFIGS = ['model', 'fields', 'exclude', 'required_fields', 'optional_fields']\n\n    def __new__(cls, cls_name: str, bases: tuple[type[typing.Any], ...], namespace: dict[str, typing.Any], **kwargs):\n        if cls_name == 'ModelSerializer':\n            # Put `model` to the main class with `create_model()`\n            if version_info < (3, 14):\n                namespace['__annotations__'].pop('model')\n            cls.model_serializer = type(cls_name, (), namespace)\n            return super().__new__(cls)\n        # 1. Initial Check\n        cls.check_config(cls_name=cls_name, namespace=namespace)\n        config = namespace.pop('Config')\n\n        # 2. Collect `Fields`\n        field_definitions = cls.collect_fields(config=config, namespace=namespace)\n\n        # 3. Collect `pydantic.model_config`\n        model_config = cls.collect_model_config(config=config, namespace=namespace)\n        namespace |= {'model_config': model_config}\n        # 4. Create a serializer\n        return pydantic.create_model(\n            cls_name,\n            __module__=namespace['__module__'],\n            __validators__=namespace,\n            __base__=(cls.model_serializer, pydantic.BaseModel),\n            model=(typing.ClassVar[type[pydantic.BaseModel]], config.model),\n            **field_definitions,\n        )\n\n    @classmethod\n    def check_config(cls, cls_name: str, namespace: dict) -> None:\n        module = namespace['__module__']\n        address = f'{module}.{cls_name}'\n\n        # Check `Config`\n        if (config := namespace.get('Config')) is None:\n            msg = f'`class Config` is required in {address}.'\n            raise AttributeError(msg) from None\n\n        # Check `model`\n        if (model := getattr(config, 'model', None)) is None:\n            msg = f'`{cls_name}.Config.model` is required.'\n            raise AttributeError(msg) from None\n\n        # Check `model` type\n        try:\n            if not issubclass(model, (Model, pydantic.BaseModel)):\n                msg = f'`{cls_name}.Config.model` is not subclass of `panther.db.Model` or `pydantic.BaseModel`.'\n                raise AttributeError(msg) from None\n        except TypeError:\n            msg = f'`{cls_name}.Config.model` is not subclass of `panther.db.Model` or `pydantic.BaseModel`.'\n            raise AttributeError(msg) from None\n\n        # Check `fields`\n        if not hasattr(config, 'fields'):\n            msg = f'`{cls_name}.Config.fields` is required.'\n            raise AttributeError(msg) from None\n\n        if config.fields != '*':\n            for field_name in config.fields:\n                if field_name == '*':\n                    msg = f\"`{cls_name}.Config.fields.{field_name}` is not valid. Did you mean `fields = '*'`\"\n                    raise AttributeError(msg) from None\n\n                if field_name not in model.model_fields:\n                    msg = f'`{cls_name}.Config.fields.{field_name}` is not in `{model.__name__}.model_fields`'\n                    raise AttributeError(msg) from None\n\n        # Check `required_fields`\n        if not hasattr(config, 'required_fields'):\n            config.required_fields = []\n\n        if config.required_fields != '*':\n            for required in config.required_fields:\n                if required not in model.model_fields:\n                    msg = f'`{cls_name}.Config.required_fields.{required}` is not valid.'\n                    raise AttributeError(msg) from None\n\n                if config.fields != '*' and required not in config.fields:\n                    msg = f'`{cls_name}.Config.required_fields.{required}` is not defined in `Config.fields`.'\n                    raise AttributeError(msg) from None\n\n        # Check `optional_fields`\n        if not hasattr(config, 'optional_fields'):\n            config.optional_fields = []\n\n        if config.optional_fields != '*':\n            for optional in config.optional_fields:\n                if optional not in model.model_fields:\n                    msg = f'`{cls_name}.Config.optional_fields.{optional}` is not valid.'\n                    raise AttributeError(msg) from None\n\n                if config.fields != '*' and optional not in config.fields:\n                    msg = f'`{cls_name}.Config.optional_fields.{optional}` is not defined in `Config.fields`.'\n                    raise AttributeError(msg) from None\n\n        # Check `required_fields` and `optional_fields` together\n        if (config.optional_fields == '*' and config.required_fields != []) or (\n            config.required_fields == '*' and config.optional_fields != []\n        ):\n            msg = (\n                f'`{cls_name}.Config.optional_fields` and '\n                f\"`{cls_name}.Config.required_fields` can't include same fields at the same time\"\n            )\n            raise AttributeError(msg) from None\n        for optional in config.optional_fields:\n            for required in config.required_fields:\n                if optional == required:\n                    msg = (\n                        f\"`{optional}` can't be in `{cls_name}.Config.optional_fields` and \"\n                        f'`{cls_name}.Config.required_fields` at the same time'\n                    )\n                    raise AttributeError(msg) from None\n\n        # Check `exclude`\n        if not hasattr(config, 'exclude'):\n            config.exclude = []\n\n        for field_name in config.exclude:\n            if field_name not in model.model_fields:\n                msg = f'`{cls_name}.Config.exclude.{field_name}` is not valid.'\n                raise AttributeError(msg) from None\n\n            if config.fields != '*' and field_name not in config.fields:\n                msg = f'`{cls_name}.Config.exclude.{field_name}` is not defined in `Config.fields`.'\n                raise AttributeError(msg) from None\n\n    @classmethod\n    def collect_fields(cls, config: typing.Callable, namespace: dict) -> dict:\n        field_definitions = {}\n\n        # Define `fields`\n        if config.fields == '*':\n            for field_name, field in config.model.model_fields.items():\n                field_definitions[field_name] = (field.annotation, field)\n        else:\n            for field_name in config.fields:\n                field_definitions[field_name] = (\n                    config.model.model_fields[field_name].annotation,\n                    config.model.model_fields[field_name],\n                )\n\n        # Apply `exclude`\n        for field_name in config.exclude:\n            del field_definitions[field_name]\n\n        # Apply `required_fields`\n        if config.required_fields == '*':\n            for value in field_definitions.values():\n                value[1].default = pydantic_core._pydantic_core.PydanticUndefined\n        else:\n            for field_name in config.required_fields:\n                field_definitions[field_name][1].default = pydantic_core._pydantic_core.PydanticUndefined\n\n        # Apply `optional_fields`\n        if config.optional_fields == '*':\n            for value in field_definitions.values():\n                value[1].default = value[0]()\n        else:\n            for field_name in config.optional_fields:\n                if issubclass(field_definitions[field_name][0], datetime.datetime):\n                    field_definitions[field_name][1].default = timezone_now()\n                else:\n                    field_definitions[field_name][1].default = field_definitions[field_name][0]()\n        # Collect and Override `Class Fields`\n        if version_info < (3, 14):\n            annotations = namespace.pop('__annotations__', {})\n        else:\n            annotations = namespace['__annotate_func__'](0) if '__annotate_func__' in namespace else {}\n        for key, value in annotations.items():\n            field_info = namespace.pop(key, pydantic.fields.FieldInfo(annotation=value))\n            field_definitions[key] = (value, field_info)\n\n        # Check Foreign Keys\n        for field_name, field_config in field_definitions.items():\n            try:\n                if issubclass(field_config[0], Model):\n                    validator = pydantic.BeforeValidator(cls.convert_str_to_model(field_config[0]))\n                    ann = typing.Annotated[field_config[0], validator]\n                    field_definitions[field_name] = (ann, field_config[1])\n            except TypeError:\n                pass\n        return field_definitions\n\n    @classmethod\n    def collect_model_config(cls, config: typing.Callable, namespace: dict) -> dict:\n        return (\n            {\n                attr: getattr(config, attr)\n                for attr in dir(config)\n                if not attr.startswith('__') and attr not in cls.KNOWN_CONFIGS\n            }\n            | namespace.pop('model_config', {})\n            | {'arbitrary_types_allowed': True}\n        )\n\n    @classmethod\n    def convert_str_to_model(cls, model_cls):\n        def _convert(v: Any) -> Any:\n            if isinstance(v, str):\n                return run_coroutine(model_cls.find_one(id=v))\n            return v\n\n        return _convert\n\n\nclass ModelSerializer(metaclass=MetaModelSerializer):\n    \"\"\"\n    A serializer class that automatically generates Pydantic models from database models.\n\n    Documentation:\n        https://pantherpy.github.io/serializer/#style-2-modelserializer\n\n    Example:\n        class PersonSerializer(ModelSerializer):\n            class Config:\n                # Required: The model class to serialize\n                model = Person\n\n                # Required: Fields to include in serialization\n                # Use '*' for all fields, or specify a list of field names\n                fields = '*'  # or ['id', 'name', 'email']\n\n                # Optional: Fields to exclude from serialization\n                # Must be a list of field names (cannot be '*')\n                exclude = ['created_date', 'updated_date']\n\n                # Optional: Fields that are required (no default value)\n                # Can be a list of field names or '*' for all fields\n                required_fields = ['first_name', 'last_name']\n\n                # Optional: Fields that are optional (with default values)\n                # Can be a list of field names or '*' for all fields\n                optional_fields = ['age', 'bio']\n    \"\"\"\n\n    model: type[pydantic.BaseModel]\n\n    async def to_response(self, instance: Any, data: dict) -> dict:\n        return data\n"
  },
  {
    "path": "panther/status.py",
    "content": "\"\"\"\nHTTP codes\nhttps://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml\nAnd RFC 2324 - https://tools.ietf.org/html/rfc2324\n\nWEBSOCKET codes\nhttps://www.iana.org/assignments/websocket/websocket.xhtml\n\"\"\"\n\nHTTP_100_CONTINUE = 100\nHTTP_101_SWITCHING_PROTOCOLS = 101\nHTTP_102_PROCESSING = 102\nHTTP_103_EARLY_HINTS = 103\nHTTP_200_OK = 200\nHTTP_201_CREATED = 201\nHTTP_202_ACCEPTED = 202\nHTTP_203_NON_AUTHORITATIVE_INFORMATION = 203\nHTTP_204_NO_CONTENT = 204\nHTTP_205_RESET_CONTENT = 205\nHTTP_206_PARTIAL_CONTENT = 206\nHTTP_207_MULTI_STATUS = 207\nHTTP_208_ALREADY_REPORTED = 208\nHTTP_226_IM_USED = 226\nHTTP_300_MULTIPLE_CHOICES = 300\nHTTP_301_MOVED_PERMANENTLY = 301\nHTTP_302_FOUND = 302\nHTTP_303_SEE_OTHER = 303\nHTTP_304_NOT_MODIFIED = 304\nHTTP_305_USE_PROXY = 305\nHTTP_306_RESERVED = 306\nHTTP_307_TEMPORARY_REDIRECT = 307\nHTTP_308_PERMANENT_REDIRECT = 308\nHTTP_400_BAD_REQUEST = 400\nHTTP_401_UNAUTHORIZED = 401\nHTTP_402_PAYMENT_REQUIRED = 402\nHTTP_403_FORBIDDEN = 403\nHTTP_404_NOT_FOUND = 404\nHTTP_405_METHOD_NOT_ALLOWED = 405\nHTTP_406_NOT_ACCEPTABLE = 406\nHTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407\nHTTP_408_REQUEST_TIMEOUT = 408\nHTTP_409_CONFLICT = 409\nHTTP_410_GONE = 410\nHTTP_411_LENGTH_REQUIRED = 411\nHTTP_412_PRECONDITION_FAILED = 412\nHTTP_413_REQUEST_ENTITY_TOO_LARGE = 413\nHTTP_414_REQUEST_URI_TOO_LONG = 414\nHTTP_415_UNSUPPORTED_MEDIA_TYPE = 415\nHTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416\nHTTP_417_EXPECTATION_FAILED = 417\nHTTP_418_IM_A_TEAPOT = 418\nHTTP_421_MISDIRECTED_REQUEST = 421\nHTTP_422_UNPROCESSABLE_ENTITY = 422\nHTTP_423_LOCKED = 423\nHTTP_424_FAILED_DEPENDENCY = 424\nHTTP_425_TOO_EARLY = 425\nHTTP_426_UPGRADE_REQUIRED = 426\nHTTP_428_PRECONDITION_REQUIRED = 428\nHTTP_429_TOO_MANY_REQUESTS = 429\nHTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431\nHTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451\nHTTP_500_INTERNAL_SERVER_ERROR = 500\nHTTP_501_NOT_IMPLEMENTED = 501\nHTTP_502_BAD_GATEWAY = 502\nHTTP_503_SERVICE_UNAVAILABLE = 503\nHTTP_504_GATEWAY_TIMEOUT = 504\nHTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505\nHTTP_506_VARIANT_ALSO_NEGOTIATES = 506\nHTTP_507_INSUFFICIENT_STORAGE = 507\nHTTP_508_LOOP_DETECTED = 508\nHTTP_510_NOT_EXTENDED = 510\nHTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511\n\n\nWS_1000_NORMAL_CLOSURE = 1000\nWS_1001_GOING_AWAY = 1001\nWS_1002_PROTOCOL_ERROR = 1002\nWS_1003_UNSUPPORTED_DATA = 1003\nWS_1007_INVALID_FRAME_PAYLOAD_DATA = 1007\nWS_1008_POLICY_VIOLATION = 1008\nWS_1009_MESSAGE_TOO_BIG = 1009\nWS_1010_MANDATORY_EXT = 1010\nWS_1011_INTERNAL_ERROR = 1011\nWS_1012_SERVICE_RESTART = 1012\nWS_1013_TRY_AGAIN_LATER = 1013\nWS_1014_BAD_GATEWAY = 1014\n\n\"\"\"\nYou can't use these status codes manually:\n\nWS_RESERVED = 1004\nWS_NO_STATUS_RCVD = 1005\nWS_ABNORMAL_CLOSURE = 1006\nWS_TLS_HANDSHAKE = 1015\n\"\"\"\n\nstatus_text = {int(x[5:8]): x[9:].replace('_', ' ').title() for x in globals() if x.startswith('HTTP')}\nstatus_text.update({int(x[3:7]): x[8:].replace('_', ' ').title() for x in globals() if x.startswith('WS')})\n\n\ndef is_informational(status_code: int):\n    return bool(100 <= status_code < 200)\n\n\ndef is_success(status_code: int):\n    return bool(200 <= status_code < 300)\n\n\ndef is_redirect(status_code: int):\n    return bool(300 <= status_code < 400)\n\n\ndef is_client_error(status_code: int):\n    return bool(400 <= status_code < 500)\n\n\ndef is_server_error(status_code: int):\n    return bool(500 <= status_code < 600)\n"
  },
  {
    "path": "panther/test.py",
    "content": "import asyncio\nfrom collections.abc import Callable\nfrom typing import Literal\n\nimport orjson as json\n\nfrom panther.response import HTMLResponse, PlainTextResponse, Response\n\n__all__ = ('APIClient', 'WebsocketClient')\n\n\nclass RequestClient:\n    def __init__(self, app: Callable):\n        self.app = app\n        self.response = b''\n\n    async def send(self, data: dict):\n        if data['type'] == 'http.response.start':\n            self.header = data\n        else:\n            self.response += data['body']\n\n    async def receive(self):\n        return {\n            'type': 'http.request',\n            'body': self.payload,\n            'more_body': False,\n        }\n\n    async def request(\n        self,\n        path: str,\n        method: Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\n        payload: bytes | dict | None,\n        headers: dict,\n        query_params: dict,\n    ) -> Response:\n        headers = [(k.encode(), str(v).encode()) for k, v in headers.items()]\n        if not path.startswith('/'):\n            path = f'/{path}'\n\n        self.payload = payload if isinstance(payload, bytes) else json.dumps(payload)\n        query_params = '&'.join(f'{k}={v}' for k, v in query_params.items())\n        scope = {\n            'type': 'http',\n            'client': ('127.0.0.1', 8585),\n            'headers': headers,\n            'method': method,\n            'path': path,\n            'raw_path': path.encode(),\n            'query_string': query_params.encode(),\n        }\n        await self.app(\n            scope=scope,\n            receive=self.receive,\n            send=self.send,\n        )\n        response_headers = {key.decode(): value.decode() for key, value in self.header['headers']}\n        cookies = [(key, value) for key, value in self.header['headers'] if key.decode() == 'Set-Cookie']\n        if response_headers['Content-Type'] == 'text/html; charset=utf-8':\n            data = self.response.decode()\n            response = HTMLResponse(data=data, status_code=self.header['status'], headers=response_headers)\n\n        elif response_headers['Content-Type'] == 'text/plain; charset=utf-8':\n            data = self.response.decode()\n            response = PlainTextResponse(data=data, status_code=self.header['status'], headers=response_headers)\n\n        elif response_headers['Content-Type'] == 'application/octet-stream':\n            data = self.response.decode()\n            response = PlainTextResponse(data=data, status_code=self.header['status'], headers=response_headers)\n\n        elif response_headers['Content-Type'] == 'application/json':\n            data = json.loads(self.response or b'null')\n            response = Response(data=data, status_code=self.header['status'], headers=response_headers)\n\n        else:\n            response = Response(data=self.response, status_code=self.header['status'], headers=response_headers)\n        response.cookies = cookies\n        return response\n\n\nclass APIClient:\n    def __init__(self, app: Callable):\n        self._app = app\n\n    async def _send_request(\n        self,\n        path: str,\n        method: Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\n        payload: dict | None,\n        headers: dict,\n        query_params: dict,\n    ) -> Response:\n        request_client = RequestClient(app=self._app)\n        return await request_client.request(\n            path=path,\n            method=method,\n            payload=payload,\n            headers=headers,\n            query_params=query_params or {},\n        )\n\n    async def options(\n        self,\n        path: str,\n        headers: dict | None = None,\n        query_params: dict | None = None,\n    ) -> Response:\n        return await self._send_request(\n            path=path,\n            method='OPTIONS',\n            payload=None,\n            headers=headers or {},\n            query_params=query_params or {},\n        )\n\n    async def get(\n        self,\n        path: str,\n        headers: dict | None = None,\n        query_params: dict | None = None,\n    ) -> Response:\n        return await self._send_request(\n            path=path,\n            method='GET',\n            payload=None,\n            headers=headers or {},\n            query_params=query_params or {},\n        )\n\n    async def post(\n        self,\n        path: str,\n        payload: bytes | dict | None = None,\n        headers: dict | None = None,\n        query_params: dict | None = None,\n        content_type: str = 'application/json',\n    ) -> Response:\n        headers = {'content-type': content_type} | (headers or {})\n        return await self._send_request(\n            path=path,\n            method='POST',\n            payload=payload,\n            headers=headers,\n            query_params=query_params or {},\n        )\n\n    async def put(\n        self,\n        path: str,\n        payload: bytes | dict | None = None,\n        headers: dict | None = None,\n        query_params: dict | None = None,\n        content_type: Literal['application/json', 'multipart/form-data'] = 'application/json',\n    ) -> Response:\n        headers = {'content-type': content_type} | (headers or {})\n        return await self._send_request(\n            path=path,\n            method='PUT',\n            payload=payload,\n            headers=headers,\n            query_params=query_params or {},\n        )\n\n    async def patch(\n        self,\n        path: str,\n        payload: bytes | dict | None = None,\n        headers: dict | None = None,\n        query_params: dict | None = None,\n        content_type: Literal['application/json', 'multipart/form-data'] = 'application/json',\n    ) -> Response:\n        headers = {'content-type': content_type} | (headers or {})\n        return await self._send_request(\n            path=path,\n            method='PATCH',\n            payload=payload,\n            headers=headers,\n            query_params=query_params or {},\n        )\n\n    async def delete(\n        self,\n        path: str,\n        headers: dict | None = None,\n        query_params: dict | None = None,\n    ) -> Response:\n        return await self._send_request(\n            path=path,\n            method='DELETE',\n            payload=None,\n            headers=headers or {},\n            query_params=query_params or {},\n        )\n\n\nclass WebsocketClient:\n    def __init__(self, app: Callable):\n        self.app = app\n        self.responses = []\n\n    async def send(self, data: dict):\n        self.responses.append(data)\n\n    async def receive(self):\n        return {'type': 'websocket.connect'}\n\n    def connect(\n        self,\n        path: str,\n        headers: dict | None = None,\n        query_params: dict | None = None,\n    ):\n        headers = [(k.encode(), str(v).encode()) for k, v in (headers or {}).items()]\n        if not path.startswith('/'):\n            path = f'/{path}'\n\n        query_params = '&'.join(f'{k}={v}' for k, v in (query_params or {}).items())\n        scope = {\n            'type': 'websocket',\n            'asgi': {'version': '3.0', 'spec_version': '2.3'},\n            'http_version': '1.1',\n            'scheme': 'ws',\n            'server': ('127.0.0.1', 8000),\n            'client': ('127.0.0.1', 55330),\n            'path': path,\n            'raw_path': path.encode(),\n            'query_string': query_params.encode(),\n            'headers': headers,\n            'subprotocols': [],\n            'state': {},\n        }\n        asyncio.run(\n            self.app(\n                scope=scope,\n                receive=self.receive,\n                send=self.send,\n            ),\n        )\n        return self.responses\n"
  },
  {
    "path": "panther/throttling.py",
    "content": "from collections import defaultdict\nfrom dataclasses import dataclass\nfrom datetime import datetime, timedelta\n\nfrom panther.db.connections import redis\nfrom panther.exceptions import ThrottlingAPIError\nfrom panther.request import Request\nfrom panther.utils import round_datetime\n\n# In-memory fallback storage for when Redis is unavailable\n_fallback_throttle_storage = defaultdict(int)\n\n\n@dataclass(repr=False, eq=False)\nclass Throttle:\n    rate: int\n    duration: timedelta\n\n    @property\n    def time_window(self) -> datetime:\n        return round_datetime(datetime.now(), self.duration)\n\n    def build_cache_key(self, request: Request) -> str:\n        \"\"\"\n        Generate a unique cache key based on time window, user or IP, and path.\n        This method is intended to be overridden by subclasses to customize throttling logic.\n        \"\"\"\n        identifier = request.user.id if request.user else request.client.ip\n        return f'{self.time_window}-{identifier}-{request.path}'\n\n    async def get_request_count(self, request: Request) -> int:\n        \"\"\"\n        Get the current request count for this key from Redis or fallback memory.\n        \"\"\"\n        key = self.build_cache_key(request)\n\n        if redis.is_connected:\n            value = await redis.get(key)\n            return int(value) if value else 0\n\n        return _fallback_throttle_storage.get(key, 0)\n\n    async def increment_request_count(self, request: Request) -> None:\n        \"\"\"\n        Increment the request count for this key and ensure TTL is set in Redis.\n        \"\"\"\n        key = self.build_cache_key(request)\n\n        if redis.is_connected:\n            count = await redis.incrby(key, amount=1)\n            if count == 1:\n                ttl = int(self.duration.total_seconds())\n                await redis.expire(key, ttl)\n        else:\n            _fallback_throttle_storage[key] += 1\n\n    async def check_and_increment(self, request: Request) -> None:\n        \"\"\"\n        Main throttling logic:\n        - Raises ThrottlingAPIError if limit exceeded.\n        - Otherwise increments the request count.\n        \"\"\"\n        count = await self.get_request_count(request)\n        remaining = self.rate - count - 1\n        reset_time = self.time_window + self.duration\n        retry_after = int((reset_time - datetime.now()).total_seconds())\n\n        if remaining < 0:\n            raise ThrottlingAPIError(\n                headers={\n                    'Retry-After': str(retry_after),\n                    'X-RateLimit-Reset': str(int(reset_time.timestamp())),\n                },\n            )\n\n        await self.increment_request_count(request)\n"
  },
  {
    "path": "panther/utils.py",
    "content": "import asyncio\nimport base64\nimport hashlib\nimport logging\nimport os\nfrom datetime import datetime, timedelta\nfrom pathlib import Path\nfrom threading import Thread\nfrom typing import ClassVar\n\nimport pytz\n\nfrom panther.configs import config\n\nlogger = logging.getLogger('panther')\n\nURANDOM_SIZE = 16\n\n\nclass Singleton(object):\n    _instances: ClassVar[dict] = {}\n\n    def __new__(cls, *args, **kwargs):\n        if cls not in cls._instances:\n            cls._instances[cls] = super().__new__(cls)\n        return cls._instances[cls]\n\n\ndef load_env(env_file: str | Path, /) -> dict[str, str]:\n    variables = {}\n\n    if env_file is None or not Path(env_file).is_file():\n        raise ValueError(f'\"{env_file}\" is not a file.') from None\n\n    with open(env_file) as file:\n        for line in file:\n            striped_line = line.strip()\n            if not striped_line.startswith('#') and '=' in striped_line:\n                key, value = striped_line.split('=', 1)\n                key = key.strip()\n                value = value.strip().strip('\"\\'')\n                if (boolean_value := value.lower()) in ['true', 'false']:\n                    variables[key] = bool(boolean_value == 'true')\n                else:\n                    variables[key] = value\n\n                # Load them as system environment variable\n                os.environ[key] = value\n    return variables\n\n\ndef generate_secret_key() -> str:\n    return base64.urlsafe_b64encode(os.urandom(32)).decode()\n\n\ndef round_datetime(dt: datetime, delta: timedelta) -> datetime:\n    \"\"\"\n    Example:\n        >>> round_datetime(datetime(2024, 7, 15, 13, 22, 11, 562159), timedelta(days=2))\n        datetime.datetime(2024, 7, 16, 0, 0)\n\n        >>> round_datetime(datetime(2024, 7, 16, 13, 22, 11, 562159), timedelta(days=2))\n        datetime.datetime(2024, 7, 16, 0, 0)\n\n        >>> round_datetime(datetime(2024, 7, 17, 13, 22, 11, 562159), timedelta(days=2))\n        datetime.datetime(2024, 7, 18, 0, 0)\n\n        >>> round_datetime(datetime(2024, 7, 18, 13, 22, 11, 562159), timedelta(days=2))\n        datetime.datetime(2024, 7, 18, 0, 0)\n\n    \"\"\"\n    return datetime.min + round((dt - datetime.min) / delta) * delta\n\n\ndef generate_hash_value_from_string(string_value: str, /) -> str:\n    # The point of this method is for maintenance, if we want to change\n    # the hash algorithm in the future, it will be easy.\n    return hashlib.sha256(string_value.encode('utf-8')).hexdigest()\n\n\ndef scrypt(password: str, salt: bytes, digest: bool = False) -> str | bytes:\n    \"\"\"\n    n: CPU/memory cost parameter – Must be a power of 2 (e.g. 1024)\n    r: Block size parameter, which fine-tunes sequential memory read size and performance. (8 is commonly used)\n    p: Parallelization parameter. (1 .. 232-1 * hLen/MFlen)\n    dk_len: Desired key length in bytes (\n        Intended output length in octets of the derived key; a positive integer satisfying dkLen ≤ (232− 1) * hLen.)\n    h_len: The length in octets of the hash function (32 for SHA256).\n    mf_len: The length in octets of the output of the mixing function (SMix below). Defined as r * 128 in RFC7914.\n    \"\"\"\n    n = 2**14  # 16384\n    r = 8\n    p = 10\n    dk_len = 64\n\n    derived_key = hashlib.scrypt(password=password.encode(), salt=salt, n=n, r=r, p=p, dklen=dk_len)\n    if digest:\n        return hashlib.md5(derived_key).hexdigest()\n    return derived_key\n\n\ndef timezone_now():\n    return datetime.now(tz=pytz.timezone(config.TIMEZONE))\n\n\ndef run_coroutine(coroutine):\n    try:\n        # Check if there's an event loop already running in this thread\n        asyncio.get_running_loop()\n    except RuntimeError:\n        # No event loop is running in this thread — safe to use asyncio.run\n        return asyncio.run(coroutine)\n\n    # Since we cannot block a running event loop with run_until_complete,\n    # we execute the coroutine in a separate thread with its own event loop.\n    result = []\n\n    def run_in_thread():\n        new_loop = asyncio.new_event_loop()\n        asyncio.set_event_loop(new_loop)\n        try:\n            result.append(new_loop.run_until_complete(coroutine))\n        finally:\n            new_loop.close()\n\n    thread = Thread(target=run_in_thread)\n    thread.start()\n    thread.join()\n    return result[0]\n"
  },
  {
    "path": "panther/websocket.py",
    "content": "from __future__ import annotations\n\nfrom typing import Callable\n\nfrom panther import status\nfrom panther._utils import ENDPOINT_WEBSOCKET\nfrom panther.base_websocket import Websocket\nfrom panther.configs import config\n\n\nclass GenericWebsocket(Websocket):\n    _endpoint_type = ENDPOINT_WEBSOCKET\n    auth: Callable | None = None\n    permissions: list = []\n\n    def __init__(self, parent):\n        self.__dict__ = parent.__dict__.copy()\n\n    async def connect(self, **kwargs):\n        \"\"\"\n        Check your conditions then `accept()` or `close()` the connection\n        \"\"\"\n\n    async def receive(self, data: str | bytes):\n        \"\"\"\n        Received `data` of connection,\n        You may want to use json.loads() on the `data`\n        \"\"\"\n\n    async def send(self, data: any = None):\n        \"\"\"\n        Send message to the client,\n        You may want to override it with your custom scenario. (not recommended)\n        \"\"\"\n        return await super().send(data=data)\n\n    async def close(self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = ''):\n        \"\"\"\n        Called whenever server or client, wants to close the connection\n        \"\"\"\n        return await super().close(code=code, reason=reason)\n\n\nasync def send_message_to_websocket(connection_id: str, data: any):\n    await config.WEBSOCKET_CONNECTIONS.publish(connection_id=connection_id, action='send', data=data)\n\n\nasync def close_websocket_connection(connection_id: str, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = ''):\n    data = {'code': code, 'reason': reason}\n    await config.WEBSOCKET_CONNECTIONS.publish(connection_id=connection_id, action='close', data=data)\n"
  },
  {
    "path": "profiler.py",
    "content": "\"\"\"\nPanther Framework Performance Profiler\n\nPrerequisites:\n- Install ali: https://github.com/nakabonne/ali\n- Install yappi: pip install yappi\n\nUsage:\n1. Run the profiler:\n   python profiler.py > benchmark_$(date +%s).txt\n\n2. Send requests:\n   ali --rate=100 http://127.0.0.1:8000\n\n3. View results in the generated output file\n\n* Any contribution or improvement on this profiler would be welcomed.\n\"\"\"\n\nimport uvicorn\nimport yappi\n\nfrom panther import Panther\nfrom panther.app import API\n\n\n@API()\nasync def hello_world():\n    return {'detail': 'Hello World'}\n\n\ndef main():\n    app = Panther(__name__, configs=__name__, urls={'': hello_world})\n    yappi.start()\n    uvicorn.run(app, access_log=False)\n    yappi.stop()\n\n    # Print function-level stats\n    stats = yappi.get_func_stats()\n    stats.print_all(columns={0: ('name', 100), 1: ('ncall', 10), 2: ('tsub', 10), 3: ('ttot', 10), 4: ('tavg', 10)})\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\n\t\"setuptools>=42\",\n\t\"wheel\"\n]\nbuild-backend = \"setuptools.build_meta\"\n\n[tool.pytest.ini_options]\nmarkers = [\n\t\"mongodb: marks mongodb tests\",\n\t\"slow: marks slow tests\"\n]\n"
  },
  {
    "path": "requirements.txt",
    "content": ".\npython-jose\nfaker\ncoverage\npytest\ncryptography~=42.0\nwebsockets\nmotor\nmkdocs\nmkdocs-material\nruff\nyappi\n"
  },
  {
    "path": "ruff.toml",
    "content": "exclude = [\n    \".venv\",\n\t\"build\",\n    \".git\",\n]\nselect = [\n    \"ALL\"\n]\nignore = [\n    \"C901\", \"N818\", \"N805\", \"D1\", \"D2\", \"D400\", \"D415\", \"D401\", \"YTT\", \"ANN\", \"S\", \"B\", \"C4\", \"DTZ\", \"EM\", \"EXE\",\n    \"FA\", \"INP\", \"PYI024\", \"PT\", \"RET503\", \"RET505\", \"ARG\", \"PTH123\", \"TD\", \"FIX\", \"PL\", \"RUF013\", \"TRY003\", \"TRY200\"\n]\n\nline-length = 120\ntarget-version = \"py310\"\n[flake8-quotes]\ninline-quotes = \"single\"\n\n[per-file-ignores]\n\"panther/main.py\" = [\"F405\", \"F403\"]\n\"panther/app.py\" = [\"SIM102\"]\n\"panther/utils.py\" = [\"UP004\", \"UP008\"]\n\"panther/cli/run_command.py\" = [\"PTH109\"]\n\"panther/cli/*\" = [\"T201\"]\n\"panther/db/models.py\" = [\"A003\", \"SIM108\"]\n\"panther/db/queries/queries.py\" = [\"SIM108\"]\n\"panther/routings.py\" = [\"SIM108\"]\n\"panther/status.py\" = [\"PLR2004\"]\n\"panther/_utils.py\" = [\"ERA001\"]\n\"example/app/apis.py\" = [\"ERA001\"]\n\"example/app/websockets.py\" = [\"T201\"]\n\"example/core/configs.py\" = [\"ERA001\"]\n\"example/app/urls.py\" = [\"F403\", \"F405\"]\n\"tests/*\" = [\"A003\"]\n\n[format]\nquote-style = \"single\"\n"
  },
  {
    "path": "setup.py",
    "content": "import re\nimport sys\n\nfrom setuptools import setup\n\n\ndef panther_version() -> str:\n    with open('panther/__init__.py') as f:\n        return re.search('__version__ = [\\'\"]([^\\'\"]+)[\\'\"]', f.read()).group(1)\n\n\nVERSION = panther_version()\nwith open('README.md', encoding='utf-8') as file:\n    DESCRIPTION = file.read()\n\nINSTALL_REQUIRES = [\n    'pantherdb~=2.3.3',\n    'orjson~=3.11.3',\n    'pydantic~=2.12.3',\n    'rich~=14.2.0',\n    'uvicorn>=0.21.0',\n    'pytz~=2025.2',\n    'Jinja2~=3.1.6',\n    'simple-ulid~=1.0.0',\n    'httptools~=0.7.1',\n]\n\nEXTRAS_REQUIRE = {\n    'full': [\n        'redis==6.2.0',\n        'motor~=3.7.1',\n        'ipython~=9.4.0',\n        'python-jose~=3.5.0',\n        'websockets~=15.0.1',\n        'cryptography~=45.0.5',\n        'watchfiles~=1.1.0',\n    ],\n}\n\nsetup(\n    name='panther',\n    version=VERSION,\n    python_requires='>=3.10',\n    author='Ali RajabNezhad',\n    author_email='alirn76@yahoo.com',\n    url='https://github.com/alirn76/panther',\n    description='Fast & Friendly, Web Framework For Building Async APIs',\n    long_description=DESCRIPTION,\n    long_description_content_type='text/markdown',\n    include_package_data=True,\n    license='BSD-3-Clause license',\n    classifiers=[\n        'Operating System :: OS Independent',\n        'Programming Language :: Python :: 3.10',\n        'Programming Language :: Python :: 3.11',\n        'Programming Language :: Python :: 3.12',\n        'Programming Language :: Python :: 3.13',\n        'Programming Language :: Python :: 3.14',\n    ],\n    entry_points={\n        'console_scripts': ['panther=panther.cli.main:start'],\n    },\n    package_data={\n        'panther': ['cli/*', 'panel/templates/*', 'openapi/templates/*'],\n    },\n    install_requires=INSTALL_REQUIRES,\n    extras_require=EXTRAS_REQUIRE,\n)\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/__main__.py",
    "content": "import argparse\nimport os\nimport sys\n\nimport pytest\nfrom _pytest.config import ExitCode\n\nTEST_DIR = os.path.dirname(__file__)\n\nTEST_SUCCESS_CONDITIONS = [ExitCode.OK, ExitCode.NO_TESTS_COLLECTED]\n\n\ndef run_test_file(path, flags: list) -> int:\n    print(f'Running {os.path.basename(path)}')\n    return pytest.main([path, *flags])\n\n\ndef main():\n    \"\"\"\n    We have to separate load of test so we can have more isolated test cases,\n    We had issue on testing `config`, some attributes been filled on the load of file,\n        for example we collect models on __init_subclass__ of their parent\n\n    Example Usage:\n        python tests --not_slow --not_mongodb\n    \"\"\"\n    parser = argparse.ArgumentParser(description='Run test files individually.')\n    # Make sure to define each flag in here, pyproject.toml and on top of the test target.\n    parser.add_argument('--mongodb', action='store_true', help='Only run mongodb tests.')\n    parser.add_argument('--not_mongodb', action='store_true', help='Does not run mongodb tests.')\n    parser.add_argument('--slow', action='store_true', help='Only run slow tests.')\n    parser.add_argument('--not_slow', action='store_true', help='Does not run slow tests.')\n    args = parser.parse_args()\n\n    files = [os.path.join(TEST_DIR, f) for f in os.listdir(TEST_DIR) if f.startswith('test_') and f.endswith('.py')]\n\n    flags = []\n    if args.not_mongodb:\n        flags.append('not mongodb')\n    if args.not_slow:\n        flags.append('not slow')\n    if args.mongodb:\n        flags.append('mongodb')\n    if args.slow:\n        flags.append('slow')\n    if flags:\n        flags = ['-m', ' and '.join(f for f in flags)]\n\n    results = [run_test_file(file, flags) for file in files]\n    for code, file in zip(results, files):\n        if code not in TEST_SUCCESS_CONDITIONS:\n            print(f'[FAIL] Some tests failed in {os.path.basename(file)}', file=sys.stderr)\n\n    if any(code not in TEST_SUCCESS_CONDITIONS for code in results):\n        sys.exit(1)\n    print('\\n[PASS] All tests passed.')\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "tests/sample_project/app/apis.py",
    "content": "from panther.app import API\n\n\n@API()\ndef first_api():\n    pass\n\n\n@API()\ndef second_api():\n    pass\n"
  },
  {
    "path": "tests/sample_project/app/models.py",
    "content": "from panther.db.models import BaseUser\n\n\nclass User(BaseUser):\n    age: int\n"
  },
  {
    "path": "tests/sample_project/app/urls.py",
    "content": "from app.apis import first_api, second_api\n\nurls = {\n    '/': first_api,\n    'second': second_api,\n}\n"
  },
  {
    "path": "tests/sample_project/core/configs.py",
    "content": "from datetime import timedelta\nfrom pathlib import Path\n\nfrom panther.throttling import Throttle\nfrom panther.utils import load_env\n\nBASE_DIR = Path(__name__).resolve().parent / 'tests/sample_project'\n\nenv = load_env(BASE_DIR / '.env')\n\nSECRET_KEY = env['SECRET_KEY']\n\nMIDDLEWARES = ['panther.middlewares.monitoring.MonitoringMiddleware']\n\nLOG_QUERIES = True\nDATABASE = {\n    'engine': {'class': 'panther.db.connections.PantherDBConnection'},\n}\nAUTHENTICATION = 'panther.authentications.JWTAuthentication'\nJWT_CONFIG = {\n    'algorithm': 'HS256',\n    'life_time': timedelta(days=2),\n    'key': SECRET_KEY,\n}\n\nURLs = 'core.urls.url_routing'\n\nUSER_MODEL = 'app.models.User'\n\nTHROTTLING = Throttle(rate=10, duration=timedelta(seconds=10))\n"
  },
  {
    "path": "tests/sample_project/core/events.py",
    "content": "from panther.events import Event\n\n\n@Event.startup\nasync def startup():\n    print('Starting Up')\n\n\n@Event.startup\nasync def shutdown():\n    print('Shutting Down')\n"
  },
  {
    "path": "tests/sample_project/core/urls.py",
    "content": "from app.urls import urls\n\nurl_routing = {\n    '': urls,\n}\n"
  },
  {
    "path": "tests/test_api_kwargs.py",
    "content": "from unittest import IsolatedAsyncioTestCase\n\nfrom panther import Panther\nfrom panther.app import API, GenericAPI\nfrom panther.configs import config\nfrom panther.request import Request\nfrom panther.test import APIClient\n\n\n@API()\nasync def request_api(request: Request):\n    assert isinstance(request, Request)\n    return True\n\n\n@API()\nasync def req_api(req: Request):\n    assert isinstance(req, Request)\n    return True\n\n\n@API()\nasync def integer_path_api(age: int):\n    assert isinstance(age, int)\n    return age\n\n\n@API()\nasync def boolean_path_api(is_alive: bool):\n    assert isinstance(is_alive, bool)\n    return is_alive\n\n\n@API()\nasync def string_path_api(name: str):\n    assert isinstance(name, str)\n    return name\n\n\n@API()\nasync def unknown_path_api(subject):\n    assert isinstance(subject, str)\n    return subject\n\n\n@API()\nasync def all_kwargs_api(request: Request, is_alive: bool, age: int, name: str, subject):\n    assert isinstance(request, Request)\n    assert isinstance(is_alive, bool)\n    assert isinstance(age, int)\n    assert isinstance(name, str)\n    assert isinstance(subject, str)\n    return is_alive, age, name, subject\n\n\n@API()\nasync def unordered_kwargs_api(subject, is_alive: bool, name: str, request: Request, age: int):\n    assert isinstance(request, Request)\n    assert isinstance(is_alive, bool)\n    assert isinstance(age, int)\n    assert isinstance(name, str)\n    assert isinstance(subject, str)\n    return is_alive, age, name, subject\n\n\nclass AllKwargsAPI(GenericAPI):\n    async def get(self, request: Request, is_alive: bool, age: int, name: str, subject):\n        assert isinstance(request, Request)\n        assert isinstance(is_alive, bool)\n        assert isinstance(age, int)\n        assert isinstance(name, str)\n        assert isinstance(subject, str)\n        return is_alive, age, name, subject\n\n\nurls = {\n    'request': request_api,\n    'req': req_api,\n    'integer/<age>/': integer_path_api,\n    'boolean/<is_alive>/': boolean_path_api,\n    'string/<name>/': string_path_api,\n    'unknown/<subject>/': unknown_path_api,\n    'all/<is_alive>/<age>/<name>/<subject>/': all_kwargs_api,\n    'unordered/<is_alive>/<age>/<name>/<subject>/': unordered_kwargs_api,\n    'class/<is_alive>/<age>/<name>/<subject>/': AllKwargsAPI,\n}\n\n\nclass TestKwargs(IsolatedAsyncioTestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        app = Panther(__name__, configs=__name__, urls=urls)\n        cls.client = APIClient(app=app)\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n\n    async def test_request(self):\n        res = await self.client.get('request')\n        assert res.status_code == 200\n\n    async def test_req(self):\n        \"\"\"Make sure it check the type of kwargs not the name of variable\"\"\"\n        res = await self.client.get('req')\n        assert res.status_code == 200\n\n    async def test_integer(self):\n        res = await self.client.get('integer/10/')\n        assert res.status_code == 200\n        assert res.data == 10\n\n    async def test_invalid_integer(self):\n        res = await self.client.get('integer/ali/')\n        assert res.status_code == 400\n        assert res.data == {'detail': 'Path variable `ali` is not `int`'}\n\n    async def test_boolean_true(self):\n        re1 = await self.client.get('boolean/true/')\n        assert re1.status_code == 200\n        assert re1.data == True\n\n        res2 = await self.client.get('boolean/TRUE/')\n        assert res2.status_code == 200\n        assert res2.data == True\n\n        res3 = await self.client.get('boolean/true/')\n        assert res3.status_code == 200\n        assert res3.data == True\n\n    async def test_boolean_false(self):\n        res1 = await self.client.get('boolean/false/')\n        assert res1.status_code == 200\n        assert res1.data == False\n\n        res2 = await self.client.get('boolean/FALSE/')\n        assert res2.status_code == 200\n        assert res2.data == False\n\n        res3 = await self.client.get('boolean/False/')\n        assert res3.status_code == 200\n        assert res3.data == False\n\n        res4 = await self.client.get('boolean/0/')\n        assert res4.status_code == 200\n        assert res4.data == False\n\n    async def test_invalid_boolean(self):\n        res = await self.client.get('boolean/ali/')\n        assert res.status_code == 400\n        assert res.data == {'detail': 'Path variable `ali` is not `bool`'}\n\n    async def test_string(self):\n        res = await self.client.get('string/ali/')\n        assert res.status_code == 200\n        assert res.data == 'ali'\n\n    async def test_unknown(self):\n        res = await self.client.get('unknown/test/')\n        assert res.status_code == 200\n        assert res.data == 'test'\n\n    async def test_all(self):\n        res = await self.client.get('all/true/20/ali/test')\n        assert res.status_code == 200\n        assert res.data == [True, 20, 'ali', 'test']\n\n    async def test_unordered(self):\n        res = await self.client.get('unordered/true/20/ali/test')\n        assert res.status_code == 200\n        assert res.data == [True, 20, 'ali', 'test']\n\n    async def test_class_based(self):\n        res = await self.client.get('class/true/20/ali/test')\n        assert res.status_code == 200\n        assert res.data == [True, 20, 'ali', 'test']\n"
  },
  {
    "path": "tests/test_authentication.py",
    "content": "from pathlib import Path\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom panther import Panther\nfrom panther.app import API, GenericAPI\nfrom panther.authentications import CookieJWTAuthentication, QueryParamJWTAuthentication\nfrom panther.configs import config\nfrom panther.db.models import BaseUser\nfrom panther.request import Request\nfrom panther.test import APIClient\n\n\nclass CustomAuth:\n    async def __call__(self, request: Request):\n        return 'THIS IS USER'\n\n\n@API()\nasync def auth_required_api(request: Request):\n    return request.user\n\n\n@API()\nasync def refresh_token_api(request: Request):\n    return await request.user.refresh_tokens()\n\n\n@API()\nasync def logout_api(request: Request):\n    return await request.user.logout()\n\n\n@API(auth=CustomAuth)\nasync def custom_auth_api(request: Request):\n    return request.user\n\n\nasync def custom_auth(req):\n    return 'THIS IS USER'\n\n\n@API(auth=custom_auth)\nasync def custom_auth_function_api(request: Request):\n    return request.user\n\n\nurls = {\n    'auth-required': auth_required_api,\n    'refresh-token': refresh_token_api,\n    'logout': logout_api,\n    'custom-auth': custom_auth_api,\n    'custom-auth-function': custom_auth_function_api,\n}\n\n\nclass User(BaseUser):\n    username: str\n    password: str\n\n\nAUTHENTICATION = 'panther.authentications.JWTAuthentication'\nSECRET_KEY = 'hvdhRspoTPh1cJVBHcuingQeOKNc1uRhIP2k7suLe2g='\nDB_PATH = 'test.pdb'\nDATABASE = {\n    'engine': {\n        'class': 'panther.db.connections.PantherDBConnection',\n        'path': DB_PATH,\n    },\n}\nUSER_MODEL = 'tests.test_authentication.User'\n\n\nclass TestJWTAuthentication(IsolatedAsyncioTestCase):\n    SHORT_TOKEN = {'Authorization': 'Token TOKEN'}\n    NOT_ENOUGH_SEGMENT_TOKEN = {'Authorization': 'Bearer XXX'}\n    JUST_BEARER_TOKEN = {'Authorization': 'Bearer'}\n    BAD_UNICODE_TOKEN = {'Authorization': 'Bearer علی'}\n    BAD_SIGNATURE_TOKEN = {\n        'Authorization': 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MX0.JAWUkAU2mWhxcd6MS8r9pd44yBIfkEBmpr3WLeqIccM',\n    }\n    TOKEN_WITHOUT_USER_ID = {\n        'Authorization': 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MX0.PpyXW0PgmGSPaaNirm_Ei4Y2fw9nb4TN26RN1u9RHSo',\n    }\n    TOKEN = {\n        'Authorization': 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.AF3nsj8IQ6t0ncqIx4quoyPfYaZ-pqUOW4z_euUztPM',\n    }\n\n    def setUp(self) -> None:\n        app = Panther(__name__, configs=__name__, urls=urls)\n        self.client = APIClient(app=app)\n\n    def tearDown(self) -> None:\n        Path(DB_PATH).unlink()\n        config.refresh()\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n\n    async def test_user_auth_required_without_auth_class(self):\n        auth_config = config.AUTHENTICATION\n        config.AUTHENTICATION = None\n        with self.assertNoLogs(level='ERROR'):\n            res = await self.client.get('auth-required')\n        assert res.status_code == 200\n        assert res.data is None\n        config.AUTHENTICATION = auth_config\n\n    async def test_user_auth_required_without_token(self):\n        with self.assertNoLogs(level='ERROR'):\n            res = await self.client.get('auth-required')\n\n        assert res.status_code == 200\n        assert res.data is None\n\n    async def test_user_auth_required_with_bad_token_1(self):\n        with self.assertLogs(level='ERROR') as captured:\n            res = await self.client.get('auth-required', headers=self.SHORT_TOKEN)\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == 'JWTAuthentication Error: \"Authorization keyword is not valid\"'\n        assert res.status_code == 401\n        assert res.data['detail'] == 'Authentication Error'\n\n    async def test_user_auth_required_with_bad_token2(self):\n        with self.assertLogs(level='ERROR') as captured:\n            res = await self.client.get('auth-required', headers=self.JUST_BEARER_TOKEN)\n\n        assert len(captured.records) == 1\n        assert (\n            captured.records[0].getMessage() == 'JWTAuthentication Error: \"Authorization header must contain 2 parts\"'\n        )\n        assert res.status_code == 401\n        assert res.data['detail'] == 'Authentication Error'\n\n    async def test_user_auth_required_with_bad_token3(self):\n        with self.assertLogs(level='ERROR') as captured:\n            res = await self.client.get('auth-required', headers=self.BAD_UNICODE_TOKEN)\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == (\n            \"JWTAuthentication Error: \\\"'latin-1' codec can't encode characters in position 0-2: \"\n            'ordinal not in range(256)\"'\n        )\n        assert res.status_code == 401\n        assert res.data['detail'] == 'Authentication Error'\n\n    async def test_user_auth_required_with_bad_token4(self):\n        with self.assertLogs(level='ERROR') as captured:\n            res = await self.client.get('auth-required', headers=self.NOT_ENOUGH_SEGMENT_TOKEN)\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == 'JWTAuthentication Error: \"Not enough segments\"'\n        assert res.status_code == 401\n        assert res.data['detail'] == 'Authentication Error'\n\n    async def test_user_auth_required_with_invalid_token_signature(self):\n        with self.assertLogs(level='ERROR') as captured:\n            res = await self.client.get('auth-required', headers=self.BAD_SIGNATURE_TOKEN)\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == 'JWTAuthentication Error: \"Signature verification failed.\"'\n        assert res.status_code == 401\n        assert res.data['detail'] == 'Authentication Error'\n\n    async def test_user_auth_required_with_token_without_user_id(self):\n        with self.assertLogs(level='ERROR') as captured:\n            res = await self.client.get('auth-required', headers=self.TOKEN_WITHOUT_USER_ID)\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == 'JWTAuthentication Error: \"Payload does not have `user_id`\"'\n        assert res.status_code == 401\n        assert res.data['detail'] == 'Authentication Error'\n\n    async def test_user_auth_required_with_token_user_not_found(self):\n        with self.assertLogs(level='ERROR') as captured:\n            res = await self.client.get('auth-required', headers=self.TOKEN)\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == 'JWTAuthentication Error: \"User not found\"'\n        assert res.status_code == 401\n        assert res.data['detail'] == 'Authentication Error'\n\n    async def test_user_auth_required_with_token(self):\n        user = await User.insert_one(username='Username', password='Password')\n        tokens = await user.login()\n\n        with self.assertNoLogs(level='ERROR'):\n            res = await self.client.get('auth-required', headers={'Authorization': f'Bearer {tokens[\"access_token\"]}'})\n\n        expected_response = {\n            'id': user.id,\n            'username': 'Username',\n            'password': 'Password',\n            'last_login': user.last_login.isoformat(),\n            'date_created': user.date_created.isoformat(),\n        }\n        assert res.status_code == 200\n        assert res.data == expected_response\n\n    async def test_invalid_refresh_token(self):\n        user = await User.insert_one(username='Username', password='Password')\n        tokens = await user.login()\n\n        with self.assertLogs(level='ERROR') as captured:\n            res = await self.client.get('refresh-token', headers={'Authorization': f'Bearer {tokens[\"access_token\"]}'})\n\n        assert len(captured.records) == 1\n        assert (\n            captured.records[0].getMessage()\n            == 'JWTAuthentication Error: \"Invalid token type; expected `refresh` token.\"'\n        )\n        assert res.status_code == 401\n        assert res.data['detail'] == 'Authentication Error'\n\n    async def test_refresh_token(self):\n        user = await User.insert_one(username='Username', password='Password')\n        tokens = await user.login()\n\n        with self.assertLogs(level='ERROR') as captured:\n            res = await self.client.get('refresh-token', headers={'Authorization': f'Bearer {tokens[\"refresh_token\"]}'})\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == 'Redis is not connected; token revocation is not effective.'\n\n        assert res.status_code == 200\n        assert res.data.keys() == {'access_token', 'refresh_token'}\n\n    async def test_cookie_authentication_without_token(self):\n        auth_config = config.AUTHENTICATION\n        config.AUTHENTICATION = CookieJWTAuthentication\n\n        user = await User.insert_one(username='Username', password='Password')\n        tokens = await user.login()\n\n        with self.assertNoLogs(level='ERROR'):\n            res = await self.client.get('auth-required', headers={'Authorization': f'Bearer {tokens[\"access_token\"]}'})\n\n        assert res.status_code == 200\n        assert res.data is None\n\n        config.AUTHENTICATION = auth_config\n\n    async def test_cookie_authentication_invalid_token(self):\n        auth_config = config.AUTHENTICATION\n        config.AUTHENTICATION = CookieJWTAuthentication\n\n        user = await User.insert_one(username='Username', password='Password')\n        tokens = await user.login()\n\n        with self.assertLogs(level='ERROR') as captured:\n            res = await self.client.get('auth-required', {'cookie': f'access_token=s'})\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == 'CookieJWTAuthentication Error: \"Not enough segments\"'\n        assert res.status_code == 401\n        assert res.data['detail'] == 'Authentication Error'\n\n        config.AUTHENTICATION = auth_config\n\n    async def test_cookie_authentication(self):\n        auth_config = config.AUTHENTICATION\n        config.AUTHENTICATION = CookieJWTAuthentication\n\n        user = await User.insert_one(username='Username', password='Password')\n        tokens = await user.login()\n\n        with self.assertNoLogs(level='ERROR'):\n            res = await self.client.get('auth-required', headers={'cookie': f'access_token={tokens[\"access_token\"]}'})\n\n        expected_response = {\n            'id': user.id,\n            'username': 'Username',\n            'password': 'Password',\n            'last_login': user.last_login.isoformat(),\n            'date_created': user.date_created.isoformat(),\n        }\n        assert res.status_code == 200\n        assert res.data == expected_response\n\n        config.AUTHENTICATION = auth_config\n\n    async def test_cookie_invalid_refresh_token(self):\n        auth_config = config.AUTHENTICATION\n        config.AUTHENTICATION = CookieJWTAuthentication\n\n        user = await User.insert_one(username='Username', password='Password')\n        tokens = await user.login()\n\n        with self.assertLogs(level='ERROR') as captured:\n            res = await self.client.get('refresh-token', headers={'cookie': f'access_token={tokens[\"access_token\"]}'})\n\n        assert len(captured.records) == 1\n        assert (\n            captured.records[0].getMessage()\n            == 'CookieJWTAuthentication Error: \"Invalid token type; expected `refresh` token.\"'\n        )\n        assert res.status_code == 401\n        assert res.data['detail'] == 'Authentication Error'\n\n        config.AUTHENTICATION = auth_config\n\n    async def test_cookie_refresh_token(self):\n        auth_config = config.AUTHENTICATION\n        config.AUTHENTICATION = CookieJWTAuthentication\n\n        user = await User.insert_one(username='Username', password='Password')\n        tokens = await user.login()\n\n        with self.assertLogs(level='ERROR') as captured:\n            res = await self.client.get(\n                'refresh-token',\n                headers={'cookie': f'access_token={tokens[\"access_token\"]}; refresh_token={tokens[\"refresh_token\"]}'},\n            )\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == 'Redis is not connected; token revocation is not effective.'\n        assert res.status_code == 200\n        assert res.data.keys() == {'access_token', 'refresh_token'}\n\n        config.AUTHENTICATION = auth_config\n\n    async def test_query_param_authentication_without_token(self):\n        auth_config = config.AUTHENTICATION\n        config.AUTHENTICATION = QueryParamJWTAuthentication\n\n        with self.assertNoLogs(level='ERROR'):\n            res = await self.client.get('auth-required')\n\n        assert res.status_code == 200\n        assert res.data is None\n\n        config.AUTHENTICATION = auth_config\n\n    async def test_query_param_authentication(self):\n        auth_config = config.AUTHENTICATION\n        config.AUTHENTICATION = QueryParamJWTAuthentication\n\n        user = await User.insert_one(username='Username', password='Password')\n        tokens = await user.login()\n\n        with self.assertNoLogs(level='ERROR'):\n            res = await self.client.get('auth-required', query_params={'authorization': tokens['access_token']})\n\n        expected_response = {\n            'id': user.id,\n            'username': 'Username',\n            'password': 'Password',\n            'last_login': user.last_login.isoformat(),\n            'date_created': user.date_created.isoformat(),\n        }\n        assert res.status_code == 200\n        assert res.data == expected_response\n\n        config.AUTHENTICATION = auth_config\n\n    async def test_query_param_invalid_refresh_token(self):\n        auth_config = config.AUTHENTICATION\n        config.AUTHENTICATION = QueryParamJWTAuthentication\n\n        user = await User.insert_one(username='Username', password='Password')\n        tokens = await user.login()\n\n        with self.assertLogs(level='ERROR') as captured:\n            res = await self.client.get('refresh-token', query_params={'authorization': tokens['access_token']})\n\n        assert len(captured.records) == 1\n        assert (\n            captured.records[0].getMessage()\n            == 'QueryParamJWTAuthentication Error: \"Invalid token type; expected `refresh` token.\"'\n        )\n        assert res.status_code == 401\n        assert res.data['detail'] == 'Authentication Error'\n\n        config.AUTHENTICATION = auth_config\n\n    async def test_query_param_refresh_token(self):\n        auth_config = config.AUTHENTICATION\n        config.AUTHENTICATION = QueryParamJWTAuthentication\n\n        user = await User.insert_one(username='Username', password='Password')\n        tokens = await user.login()\n\n        with self.assertLogs(level='ERROR') as captured:\n            res = await self.client.get('refresh-token', query_params={'authorization': tokens['refresh_token']})\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == 'Redis is not connected; token revocation is not effective.'\n        assert res.status_code == 200\n        assert res.data.keys() == {'access_token', 'refresh_token'}\n\n        config.AUTHENTICATION = auth_config\n\n    async def test_custom_auth_class(self):\n        with self.assertNoLogs(level='ERROR'):\n            res = await self.client.get('custom-auth')\n\n        assert res.status_code == 200\n        assert res.data == 'THIS IS USER'\n\n    async def test_custom_auth_function(self):\n        with self.assertNoLogs(level='ERROR'):\n            res = await self.client.get('custom-auth-function')\n\n        assert res.status_code == 200\n        assert res.data == 'THIS IS USER'\n\n    async def test_invalid_custom_auth_class(self):\n        class SyncCustomAuth:\n            def __call__(self, request: Request):\n                return 'THIS IS USER'\n\n        try:\n\n            @API(auth=SyncCustomAuth)\n            async def invalid_custom_auth_api(request: Request):\n                return request.user\n        except Exception as e:\n            assert e.args[0] == 'SyncCustomAuth.__call__() should be `async`'\n        else:\n            assert False\n\n    async def test_no_param_custom_auth_class(self):\n        class InvalidCustomAuth:\n            async def __call__(self):\n                return 'THIS IS USER'\n\n        try:\n\n            @API(auth=InvalidCustomAuth)\n            async def invalid_custom_auth_api(request: Request):\n                return request.user\n        except Exception as e:\n            assert e.args[0] == 'InvalidCustomAuth.__call__() requires 2 positional argument(s) (self, request).'\n        else:\n            assert False\n\n    async def test_invalid_custom_auth_function(self):\n        def auth(req: str):\n            pass\n\n        try:\n\n            @API(auth=auth)\n            async def invalid_custom_auth_api(request: Request):\n                return request.user\n        except Exception as e:\n            assert e.args[0] == 'auth() should be `async`'\n        else:\n            assert False\n\n    async def test_no_param_custom_auth_function(self):\n        def auth1():\n            pass\n\n        try:\n\n            @API(auth=auth1)\n            async def invalid_custom_auth_api(request: Request):\n                return request.user\n        except Exception as e:\n            assert e.args[0] == 'auth1() requires 1 positional argument(s) (request).'\n        else:\n            assert False\n\n    async def test_class_based_api_invalid_auth_type(self):\n        def auth():\n            pass\n\n        try:\n\n            class MyAPI(GenericAPI):\n                auth = True\n        except Exception as e:\n            assert e.args[0] == (\n                '`bool` is not valid for authentication, it should be a callable, a Class with __call__ '\n                'method or a single function.'\n            )\n        else:\n            assert False\n\n    async def test_class_based_api_sync_function_auth(self):\n        def auth_func(req):\n            pass\n\n        try:\n\n            class MyAPI(GenericAPI):\n                auth = auth_func\n        except Exception as e:\n            assert e.args[0] == 'auth_func() should be `async`'\n        else:\n            assert False\n\n    async def test_class_based_api_no_param_function_auth(self):\n        def auth_func2():\n            pass\n\n        try:\n\n            class MyAPI(GenericAPI):\n                auth = auth_func2\n        except Exception as e:\n            assert e.args[0] == 'auth_func2() requires 1 positional argument(s) (request).'\n        else:\n            assert False\n\n    async def test_class_based_api_sync_class_auth(self):\n        class CustomAuth1:\n            def __call__(self, req):\n                pass\n\n        try:\n\n            class MyAPI(GenericAPI):\n                auth = CustomAuth1\n        except Exception as e:\n            assert e.args[0] == 'CustomAuth1.__call__() should be `async`'\n        else:\n            assert False\n\n    async def test_class_based_api_no_param_class_auth(self):\n        class CustomAuth2:\n            def __call__(self):\n                pass\n\n        try:\n\n            class MyAPI(GenericAPI):\n                auth = CustomAuth2\n        except Exception as e:\n            assert e.args[0] == 'CustomAuth2.__call__() requires 2 positional argument(s) (self, request).'\n        else:\n            assert False\n\n    async def test_class_based_api_class_without_call(self):\n        class CustomAuth3:\n            pass\n\n        try:\n\n            class MyAPI(GenericAPI):\n                auth = CustomAuth3\n        except Exception as e:\n            assert e.args[0] == 'CustomAuth3 must implement __call__() method.'\n        else:\n            assert False\n\n    async def test_class_based_api_class_with_initiated_auth(self):\n        class CustomAuth4:\n            pass\n\n        try:\n\n            class MyAPI(GenericAPI):\n                auth = CustomAuth4()\n        except Exception as e:\n            assert (\n                e.args[0]\n                == '`CustomAuth4` is not valid for authentication, it should be a callable, a Class with __call__ method or a single function.'\n            )\n        else:\n            assert False\n\n    async def test_logout(self):\n        user = await User.insert_one(username='Username', password='Password')\n        tokens = await user.login()\n\n        with self.assertLogs(level='ERROR') as captured:\n            res = await self.client.get('logout', headers={'Authorization': f'Bearer {tokens[\"access_token\"]}'})\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == 'Redis is not connected; token revocation is not effective.'\n        assert res.status_code == 200\n\n        # TODO: We have to implement a mini redis to work with its functionalities even when its not connected.\n        # with self.assertLogs(level='ERROR') as captured:\n        #     res = await self.client.get('logout', headers={'Authorization': f'Bearer {tokens[\"access_token\"]}'})\n        # assert res.status_code == 401\n"
  },
  {
    "path": "tests/test_background_task_event_loop_isolation.py",
    "content": "import asyncio\nfrom unittest import TestCase\nfrom threading import Event, Thread\n\nimport panther.background_tasks as background_tasks\nfrom panther.background_tasks import BackgroundTask\n\n\ndef _set_application_loop(loop: asyncio.AbstractEventLoop | None) -> None:\n    if hasattr(background_tasks, 'register_application_event_loop'):\n        background_tasks.register_application_event_loop(loop)\n    else:\n        setattr(background_tasks, '_application_event_loop', loop)\n\n\nasync def _create_loop_bound_future(loop: asyncio.AbstractEventLoop) -> asyncio.Future:\n    return loop.create_future()\n\n\nclass TestBackgroundTaskEventLoopIsolation(TestCase):\n    def test_async_background_task_avoids_event_loop_mismatch(self):\n        previous_loop = getattr(background_tasks, '_application_event_loop', None)\n        app_loop = asyncio.new_event_loop()\n        app_loop_ready = Event()\n        app_loop_closed = Event()\n\n        def run_application_loop():\n            asyncio.set_event_loop(app_loop)\n            app_loop_ready.set()\n            app_loop.run_forever()\n            app_loop.close()\n            app_loop_closed.set()\n\n        loop_thread = Thread(target=run_application_loop, daemon=True)\n        loop_thread.start()\n        assert app_loop_ready.wait(timeout=1), 'Application event loop did not start in time.'\n\n        try:\n            _set_application_loop(app_loop)\n            loop_bound_future = asyncio.run_coroutine_threadsafe(\n                _create_loop_bound_future(app_loop), app_loop\n            ).result(timeout=1)\n            results = []\n\n            async def consume_loop_bound_future(_results):\n                await loop_bound_future\n                _results.append('completed')\n\n            app_loop.call_soon_threadsafe(app_loop.call_later, 0.01, loop_bound_future.set_result, 'ok')\n            BackgroundTask(consume_loop_bound_future, results)()\n\n            assert results == ['completed']\n        finally:\n            _set_application_loop(previous_loop)\n            app_loop.call_soon_threadsafe(app_loop.stop)\n            loop_thread.join(timeout=1)\n            assert app_loop_closed.wait(timeout=1), 'Application event loop did not stop in time.'\n"
  },
  {
    "path": "tests/test_background_tasks.py",
    "content": "import datetime\nimport time\nfrom unittest import TestCase\n\nimport pytest\n\nfrom panther.background_tasks import BackgroundTask, BackgroundTasks, WeekDay, _background_tasks\nfrom panther.configs import config\nfrom panther.utils import Singleton, timezone_now\n\n\n@pytest.mark.slow\nclass TestBackgroundTasks(TestCase):\n    def setUp(self):\n        self.obj = BackgroundTasks()\n        config.BACKGROUND_TASKS = True\n\n    def tearDown(self):\n        del Singleton._instances[BackgroundTasks]\n        config.BACKGROUND_TASKS = False\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n\n    def test_background_tasks_singleton(self):\n        new_obj = BackgroundTasks()\n        assert id(self.obj) == id(new_obj)\n\n    def test_initialization(self):\n        assert hasattr(self.obj, 'initialize') is True\n        assert self.obj._initialized is False\n\n        self.obj.initialize()\n        assert self.obj._initialized is True\n        assert self.obj.tasks == []\n\n    def test_add_single_task(self):\n        def func():\n            pass\n\n        task = BackgroundTask(func)\n        self.obj.initialize()\n        self.obj.add_task(task)\n        assert self.obj.tasks == [task]\n\n    def test_add_wrong_task(self):\n        def func():\n            pass\n\n        self.obj.initialize()\n        assert self.obj.tasks == []\n\n        with self.assertLogs() as captured:\n            self.obj.add_task(func)\n\n        assert len(captured.records) == 1\n        assert (\n            captured.records[0].getMessage() == f'`{func.__name__}` '\n            f'should be instance of `background_tasks.BackgroundTask`'\n        )\n        assert self.obj.tasks == []\n\n    def test_add_task_with_false_background_task(self):\n        numbers = []\n\n        def func(_numbers):\n            _numbers.append(1)\n\n        task = BackgroundTask(func, numbers)\n        with self.assertLogs() as captured:\n            self.obj.add_task(task)\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == 'Task will be ignored, `BACKGROUND_TASKS` is not True in `configs`'\n        assert self.obj.tasks == []\n\n    def test_add_task_with_args(self):\n        numbers = []\n\n        def func(_numbers):\n            _numbers.append(1)\n\n        task = BackgroundTask(func, numbers)\n        self.obj.initialize()\n        self.obj.add_task(task)\n        assert self.obj.tasks == [task]\n        time.sleep(2)\n        assert len(numbers) == 1\n\n    def test_add_task_with_kwargs(self):\n        numbers = []\n\n        def func(_numbers):\n            _numbers.append(1)\n\n        task = BackgroundTask(func, _numbers=numbers)\n        self.obj.initialize()\n        self.obj.add_task(task)\n        assert self.obj.tasks == [task]\n        time.sleep(2)\n        assert len(numbers) == 1\n\n    def test_add_task_with_custom_interval(self):\n        numbers = []\n\n        def func(_numbers):\n            _numbers.append(1)\n\n        task = BackgroundTask(func, numbers).every_seconds().interval(3)\n        self.obj.initialize()\n        self.obj.add_task(task)\n        assert self.obj.tasks == [task]\n        time.sleep(4)\n        assert len(numbers) == 3\n\n    def test_add_task_with_custom_interval_default_timedelta(self):\n        numbers = []\n\n        def func(_numbers):\n            _numbers.append(1)\n\n        task = BackgroundTask(func, numbers).interval(2)\n        self.obj.initialize()\n        self.obj.add_task(task)\n        assert self.obj.tasks == [task]\n        time.sleep(3)\n        # default timedelta is 1 minute, so it can't complete the intervals in 2 seconds\n        assert len(numbers) == 1\n\n    def test_add_multiple_tasks(self):\n        numbers = []\n\n        def func1(_numbers):\n            _numbers.append(1)\n\n        def func2(_numbers):\n            _numbers.append(1)\n\n        task2 = BackgroundTask(func1, numbers)\n        task1 = BackgroundTask(func2, numbers)\n        self.obj.initialize()\n        self.obj.add_task(task1)\n        self.obj.add_task(task2)\n        assert self.obj.tasks == [task1, task2]\n        assert len(numbers) == 0\n        time.sleep(2)\n        assert len(numbers) == 2\n\n    def test_concurrent_tasks(self):\n        numbers = []\n\n        def func1(_numbers):\n            time.sleep(3)\n            _numbers.append(1)\n\n        def func2(_numbers):\n            time.sleep(3)\n            _numbers.append(1)\n\n        _background_tasks.initialize()\n        BackgroundTask(func1, numbers).submit()\n        BackgroundTask(func2, numbers).submit()\n        assert len(numbers) == 0\n        time.sleep(5)  # Initialization takes 2 second\n        assert len(numbers) == 2\n\n    def test_add_task_with_custom_every_3_seconds(self):\n        numbers = []\n\n        def func(_numbers):\n            _numbers.append(1)\n\n        task = BackgroundTask(func, numbers).every_seconds(3).interval(2)\n        self.obj.initialize()\n        self.obj.add_task(task)\n        assert self.obj.tasks == [task]\n        time.sleep(3)\n        assert len(numbers) == 1\n        time.sleep(3)\n        assert len(numbers) == 2\n\n    def test_submit_single_task(self):\n        numbers = []\n\n        def func(_numbers):\n            _numbers.append(1)\n\n        _background_tasks.initialize()\n        task = BackgroundTask(func, numbers).submit()\n        assert _background_tasks.tasks == [task]\n        assert len(numbers) == 0\n        time.sleep(2)\n        assert len(numbers) == 1\n\n    def test_task_at_3second_later(self):\n        numbers = []\n\n        def func(_numbers):\n            _numbers.append(1)\n\n        now = timezone_now()\n\n        _background_tasks.initialize()\n        BackgroundTask(func, numbers).at((now + datetime.timedelta(seconds=3)).time()).submit()\n        time.sleep(2)\n        assert len(numbers) == 0\n        time.sleep(3)\n        assert len(numbers) == 1\n\n    def test_task_on_specific_weekday(self):\n        numbers = []\n\n        def func(_numbers):\n            _numbers.append(1)\n\n        now = timezone_now()\n        week_days = {\n            0: WeekDay.MONDAY,\n            1: WeekDay.TUESDAY,\n            2: WeekDay.WEDNESDAY,\n            3: WeekDay.THURSDAY,\n            4: WeekDay.FRIDAY,\n            5: WeekDay.SATURDAY,\n            6: WeekDay.SUNDAY,\n        }\n\n        _background_tasks.initialize()\n        BackgroundTask(func, numbers).on(week_days[now.weekday()]).submit()\n        assert len(numbers) == 0\n        time.sleep(1)\n        assert len(numbers) == 1\n"
  },
  {
    "path": "tests/test_caching.py",
    "content": "import asyncio\nimport time\nfrom datetime import timedelta\nfrom unittest import IsolatedAsyncioTestCase\n\nimport pytest\n\nfrom panther import Panther\nfrom panther.app import API\nfrom panther.configs import config\nfrom panther.response import HTMLResponse\nfrom panther.test import APIClient\n\n\n@API()\nasync def without_cache_api():\n    await asyncio.sleep(0.01)\n    return {'detail': time.time()}\n\n\n@API(cache=timedelta(seconds=2))\nasync def expired_cache_api():\n    await asyncio.sleep(0.01)\n    return {'detail': time.time()}\n\n\n@API(cache=timedelta(seconds=2))\nasync def expired_cache_html_response():\n    await asyncio.sleep(0.01)\n    return HTMLResponse(data=f'<html>{time.time()}</html>')\n\n\nurls = {\n    'without-cache': without_cache_api,\n    'with-expired-cache': expired_cache_api,\n    'with-html-response-cache': expired_cache_html_response,\n}\n\n\nclass TestInMemoryCaching(IsolatedAsyncioTestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        app = Panther(__name__, configs=__name__, urls=urls)\n        cls.client = APIClient(app=app)\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n\n    async def test_without_cache(self):\n        res1 = await self.client.get('without-cache')\n        assert res1.status_code == 200\n\n        res2 = await self.client.get('without-cache')\n        assert res2.status_code == 200\n\n        assert res1.data != res2.data\n\n    async def test_with_cache_5second_exp_time(self):\n        # First Request\n        with self.assertLogs(level='INFO') as captured:\n            res1 = await self.client.get('with-expired-cache')\n        assert res1.status_code == 200\n\n        # Check Logs\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == '`cache` is not very accurate when `redis` is not connected.'\n\n        # Second Request\n        res2 = await self.client.get('with-expired-cache')\n        assert res2.status_code == 200\n\n        # Response should be cached\n        assert res1.data == res2.data\n\n        await asyncio.sleep(2)\n\n        # Third Request\n        res3 = await self.client.get('with-expired-cache')\n        assert res3.status_code == 200\n\n        # After 5 seconds we should have a new response\n        assert res1.data != res3.data\n\n    async def test_with_cache_content_type(self):\n        # First Request\n        res1 = await self.client.get('with-html-response-cache')\n        assert res1.status_code == 200\n\n        # Second Request\n        res2 = await self.client.get('with-html-response-cache')\n        assert res2.status_code == 200\n\n        # Response should be cached\n        assert res1.data == res2.data\n\n        # Check Content-Type\n        assert res1.headers['Content-Type'] == res2.headers['Content-Type']\n"
  },
  {
    "path": "tests/test_cli.py",
    "content": "import os\nimport shutil\nimport sys\nfrom io import StringIO\nfrom pathlib import Path\nfrom unittest import skipIf\nfrom unittest.async_case import IsolatedAsyncioTestCase\nfrom unittest.mock import patch\n\nfrom rich import print as rprint\n\nfrom panther import Panther\nfrom panther.cli.create_project_command import CreateProject\nfrom panther.cli.create_user_command import create_user\nfrom panther.cli.template import SINGLE_FILE_TEMPLATE, TEMPLATE\nfrom panther.configs import config\nfrom panther.db.connections import db\nfrom panther.db.models import BaseUser\n\ninteractive_cli_1_index = 0\ninteractive_cli_2_index = 0\n\nDB_PATH = 'test_db.pdb'\n\n\n# 0.ProjectName, 1.BaseDir, 2.IsSingleFile, 3.Database,\n# 4.Encryption, 5.Authentication, 6.Monitoring, 7.LogQueries, 8.AutoReformat\ndef interactive_cli_1_mock_responses(index=None):\n    global interactive_cli_1_index\n    if index is None:\n        index = interactive_cli_1_index\n    responses = ['project1', 'project1_dir', 'n', '0', 'y', 'n', 'y', 'y', 'y', 'y']\n    response = responses[index]\n    interactive_cli_1_index += 1\n    return response\n\n\ndef interactive_cli_2_mock_responses(index=None):\n    global interactive_cli_2_index\n    if index is None:\n        index = interactive_cli_2_index\n    responses = ['project2', 'project2_dir', 'y', '0', 'y', 'n', 'y', 'y', 'y', 'y']\n    response = responses[index]\n    interactive_cli_2_index += 1\n    return response\n\n\nclass TestCLI(IsolatedAsyncioTestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        sys.path.append('tests/sample_project')\n\n    def tearDown(self) -> None:\n        if db.is_defined:\n            db.session.collection('BaseUser').drop()\n\n    @classmethod\n    def tearDownClass(cls) -> None:\n        config.refresh()\n        sys.path.pop()\n\n    @skipIf(sys.platform.startswith('win'), 'Not supported in windows')\n    async def test_print_info(self):\n        with patch('sys.stdout', new=StringIO()) as fake_out1:\n            Panther(__name__)\n\n        base_dir = f'{Path(__name__).absolute().parent!s:<41}'\n        expected_value = rf\"\"\"╭────────────────────────────────────────────────────────────╮\n│     ____                 __    __                          │\n│    /\\  _`\\              /\\ \\__/\\ \\                         │\n│    \\ \\ \\L\\ \\ __      ___\\ \\ ,_\\ \\ \\___      __   _ __      │\n│     \\ \\ ,__/'__`\\  /' _ `\\ \\ \\/\\ \\  _ `\\  /'__`\\/\\`'__\\    │\n│      \\ \\ \\/\\ \\L\\.\\_/\\ \\/\\ \\ \\ \\_\\ \\ \\ \\ \\/\\  __/\\ \\ \\/     │\n│       \\ \\_\\ \\__/.\\_\\ \\_\\ \\_\\ \\__\\\\ \\_\\ \\_\\ \\____\\\\ \\_\\     │\n│        \\/_/\\/__/\\/_/\\/_/\\/_/\\/__/ \\/_/\\/_/\\/____/ \\/_/     │\n│                                                            │\n│   Redis: False                                             │\n│   Websocket: False                                         │\n│   Monitoring: True                                         │\n│   Log Queries: True                                        │\n│   Background Tasks: False                                  │\n│   Base directory: {base_dir}│\n│ * Run \"panther monitor\" in another session for Monitoring  │\n│ * You may want to install `uvloop` for better performance  │\n│   `pip install uvloop`                                     │\n╰────────────────────────────────────────────────────────────╯\"\"\"\n        with patch('sys.stdout', new=StringIO()) as fake_out2:\n            rprint(expected_value)\n        assert fake_out1.getvalue() == fake_out2.getvalue()\n\n    @patch('builtins.input', interactive_cli_1_mock_responses)\n    async def test_create_normal_template_with_interactive_cli(self):\n        CreateProject().create([])\n\n        project_path = interactive_cli_1_mock_responses(1)\n        for file_name, data in SINGLE_FILE_TEMPLATE.items():\n            sub_directory = f'{project_path}/{file_name}'\n            assert Path(sub_directory).exists()\n\n            if isinstance(data, dict):\n                for sub_file_name in data:\n                    file_path = f'{sub_directory}/{sub_file_name}'\n                    assert Path(file_path).exists()\n        shutil.rmtree(project_path)\n\n    @patch('builtins.input', interactive_cli_2_mock_responses)\n    async def test_create_single_file_template_with_interactive_cli(self):\n        CreateProject().create([])\n\n        project_path = interactive_cli_2_mock_responses(1)\n        for file_name, data in SINGLE_FILE_TEMPLATE.items():\n            sub_directory = f'{project_path}/{file_name}'\n            assert Path(sub_directory).exists()\n\n            if isinstance(data, dict):\n                for sub_file_name in data:\n                    file_path = f'{sub_directory}/{sub_file_name}'\n                    assert Path(file_path).exists()\n        shutil.rmtree(project_path)\n\n    async def test_create_on_existence_directory(self):\n        project_path = 'test-project-directory'\n        os.mkdir(project_path)\n\n        with self.assertLogs(level='ERROR') as captured_error:\n            CreateProject().create(['test_project', project_path])\n\n        with self.assertLogs(level='INFO') as captured_info:\n            CreateProject().create(['test_project', project_path])\n\n        try:\n            assert len(captured_error.records) == 1\n            assert captured_info.records[0].getMessage() == f'\"{project_path}\" directory already exists.'\n\n            assert len(captured_info.records) == 2\n            assert captured_info.records[0].getMessage() == f'\"{project_path}\" directory already exists.'\n            assert captured_info.records[1].getMessage() == 'Use \"panther -h\" for more help'\n        except AssertionError:\n            raise\n        finally:\n            os.removedirs(project_path)\n\n    async def test_create_project(self):\n        project_path = 'test-project-directory'\n        CreateProject().create(['test_project', project_path])\n\n        for file_name, data in TEMPLATE.items():\n            sub_directory = f'{project_path}/{file_name}'\n            assert Path(sub_directory).exists()\n\n            if isinstance(data, dict):\n                for sub_file_name in data:\n                    file_path = f'{sub_directory}/{sub_file_name}'\n                    assert Path(file_path).exists()\n\n        shutil.rmtree(project_path)\n\n    @patch('panther.cli.create_user_command.get_password', return_value='testpass')\n    @patch('panther.cli.create_user_command.get_username', return_value='testuser')\n    @patch('panther.cli.create_user_command.load_application_file')\n    async def test_create_user_success(self, mock_load_application_file, mock_get_username, mock_get_password):\n        global DATABASE\n        # Setup\n        mock_get_username.side_effect = ['testuser']\n        DATABASE = {'engine': {'class': 'panther.db.connections.PantherDBConnection', 'path': DB_PATH}}\n        Panther(__name__, configs=__name__, urls={})\n\n        # Run\n        with patch('sys.stdout', new=StringIO()) as fake_out:\n            create_user(['dummy.py'])\n        # Check user created\n        user = await BaseUser.find_one({'username': 'testuser'})\n        assert user is not None\n        assert user.check_password('testpass')\n        # Cleanup\n        await user.delete()\n        # Check output\n        output = fake_out.getvalue()\n        assert 'Created Successfully' in output\n\n    @patch('panther.cli.create_user_command.get_password', return_value='testpass')\n    @patch('panther.cli.create_user_command.get_username', return_value='testuser')\n    @patch('panther.cli.create_user_command.load_application_file')\n    async def test_create_user_duplicate(self, mock_load_application_file, mock_get_username, mock_get_password):\n        global DATABASE\n        # Setup\n        mock_get_username.side_effect = ['testuser', 'testuser2']\n        DATABASE = {'engine': {'class': 'panther.db.connections.PantherDBConnection', 'path': DB_PATH}}\n        Panther(__name__, configs=__name__, urls={})\n\n        # Run 1\n        create_user(['dummy.py'])\n        assert await BaseUser.exists(username='testuser')\n        assert not await BaseUser.exists(username='testuser2')\n\n        # Run 2\n        create_user(['dummy.py'])\n        assert await BaseUser.exists(username='testuser')\n        assert await BaseUser.exists(username='testuser2')\n\n    @patch('panther.cli.create_user_command.get_password', side_effect=KeyboardInterrupt)\n    @patch('panther.cli.create_user_command.get_username', return_value='testuser')\n    @patch('panther.cli.create_user_command.load_application_file')\n    async def test_create_user_keyboard_interrupt(\n        self, mock_load_application_file, mock_get_username, mock_get_password\n    ):\n        global DATABASE\n        DATABASE = {'engine': {'class': 'panther.db.connections.PantherDBConnection', 'path': DB_PATH}}\n        Panther(__name__, configs=__name__, urls={})\n\n        # Run\n        with patch('sys.stdout', new=StringIO()) as fake_out:\n            create_user(['dummy.py'])\n        output = fake_out.getvalue()\n        assert 'Keyboard Interrupt' in output\n"
  },
  {
    "path": "tests/test_config.py",
    "content": "from datetime import timedelta\nfrom pathlib import Path\nfrom unittest import TestCase\n\nimport jinja2\n\nfrom panther import Panther\nfrom panther.app import GenericAPI\nfrom panther.authentications import CookieJWTAuthentication, QueryParamJWTAuthentication\nfrom panther.base_websocket import WebsocketConnections\nfrom panther.configs import JWTConfig, config\nfrom panther.db import Model\nfrom panther.db.connections import PantherDBConnection\nfrom panther.db.queries.pantherdb_queries import BasePantherDBQuery\nfrom panther.events import Event\nfrom panther.middlewares.monitoring import MonitoringMiddleware, WebsocketMonitoringMiddleware\nfrom panther.throttling import Throttle\nfrom panther.utils import generate_secret_key\nfrom panther.websocket import GenericWebsocket\n\n\nclass User(Model):\n    pass\n\n\nclass Book(Model):\n    pass\n\n\nclass Author(Model):\n    pass\n\n\nclass DummyAPI(GenericAPI):\n    pass\n\n\nclass DummyWS(GenericWebsocket):\n    pass\n\n\n@Event.startup\ndef my_startup1():\n    pass\n\n\n@Event.startup\ndef my_startup2():\n    pass\n\n\n@Event.shutdown\ndef my_shutdown1():\n    pass\n\n\n@Event.shutdown\ndef my_shutdown2():\n    pass\n\n\nclass TestConfig(TestCase):\n    def test_loading_known_configs(self):\n        global \\\n            BASE_DIR, \\\n            MONITORING, \\\n            LOG_QUERIES, \\\n            THROTTLING, \\\n            SECRET_KEY, \\\n            MIDDLEWARES, \\\n            HTTP_MIDDLEWARES, \\\n            WS_MIDDLEWARES, \\\n            USER_MODEL, \\\n            AUTHENTICATION, \\\n            WS_AUTHENTICATION, \\\n            JWT_CONFIG, \\\n            MODELS, \\\n            FLAT_URLS, \\\n            URLS, \\\n            WEBSOCKET_CONNECTIONS, \\\n            BACKGROUND_TASKS, \\\n            HAS_WS, \\\n            TIMEZONE, \\\n            TEMPLATES_DIR, \\\n            JINJA_ENVIRONMENT, \\\n            AUTO_REFORMAT, \\\n            QUERY_ENGINE, \\\n            DATABASE\n\n        # MIDDLEWARES\n        MIDDLEWARES = ['panther.middlewares.monitoring.MonitoringMiddleware']\n        WS_MIDDLEWARES = ['panther.middlewares.monitoring.WebsocketMonitoringMiddleware']\n        LOG_QUERIES = True\n        throttle = Throttle(rate=10, duration=timedelta(seconds=10))\n        THROTTLING = throttle\n        new_secret_key = generate_secret_key()\n        SECRET_KEY = new_secret_key\n        USER_MODEL = 'tests.test_config.User'\n        AUTHENTICATION = 'panther.authentications.QueryParamJWTAuthentication'\n        WS_AUTHENTICATION = 'panther.authentications.CookieJWTAuthentication'\n        JWT_CONFIG = {'life_time': timedelta(seconds=20)}\n        BACKGROUND_TASKS = True\n        TIMEZONE = 'Asia/Tehran'\n        TEMPLATES_DIR = 'templates/'\n        AUTO_REFORMAT = True\n        DATABASE = {\n            'engine': {\n                'class': 'panther.db.connections.PantherDBConnection',\n            },\n        }\n\n        # Check before loading configs.\n        # assert Path() == config.BASE_DIR\n        assert config.MONITORING is False\n        assert config.LOG_QUERIES is False\n        assert config.THROTTLING is None\n        assert config.SECRET_KEY is None\n        assert config.HTTP_MIDDLEWARES == []\n        assert config.WS_MIDDLEWARES == []\n        assert config.USER_MODEL is None\n        assert config.AUTHENTICATION is None\n        assert config.WS_AUTHENTICATION is None\n        assert config.JWT_CONFIG is None\n        assert [User, Book, Author] == config.MODELS  # This is ok.\n        assert config.FLAT_URLS == {}\n        assert config.URLS == {}\n        assert config.WEBSOCKET_CONNECTIONS is None\n        assert config.BACKGROUND_TASKS is False\n        assert config.HAS_WS is True\n        assert config.TIMEZONE == 'UTC'\n        assert config.TEMPLATES_DIR == '.'\n        assert config.JINJA_ENVIRONMENT is None\n        assert config.AUTO_REFORMAT is False\n        assert config.QUERY_ENGINE is None\n        assert config.DATABASE is None\n\n        with self.assertNoLogs(level='ERROR'):\n            Panther(name=__name__, configs=__name__, urls={'dummy': DummyAPI, 'ws': DummyWS})\n\n        # Check vars()\n        assert [*config.vars().keys()] == [\n            'BASE_DIR',\n            'MONITORING',\n            'LOG_QUERIES',\n            'SECRET_KEY',\n            'HTTP_MIDDLEWARES',\n            'WS_MIDDLEWARES',\n            'USER_MODEL',\n            'AUTHENTICATION',\n            'WS_AUTHENTICATION',\n            'JWT_CONFIG',\n            'MODELS',\n            'FLAT_URLS',\n            'URLS',\n            'WEBSOCKET_CONNECTIONS',\n            'BACKGROUND_TASKS',\n            'HAS_WS',\n            'TIMEZONE',\n            'TEMPLATES_DIR',\n            'JINJA_ENVIRONMENT',\n            'AUTO_REFORMAT',\n            'QUERY_ENGINE',\n            'DATABASE',\n            'THROTTLING',\n            'MIDDLEWARES',\n        ]\n\n        # Check after loading configs.\n        assert Path.cwd() == config.BASE_DIR\n        assert config.MONITORING is True\n        assert config.LOG_QUERIES is True\n        assert throttle == config.THROTTLING\n        assert new_secret_key == config.SECRET_KEY\n        assert [MonitoringMiddleware] == config.HTTP_MIDDLEWARES\n        assert [WebsocketMonitoringMiddleware] == config.WS_MIDDLEWARES\n        assert config.USER_MODEL is User\n        assert config.AUTHENTICATION is QueryParamJWTAuthentication\n        assert config.WS_AUTHENTICATION is CookieJWTAuthentication\n        assert JWTConfig(key=new_secret_key, algorithm='HS256', life_time=20, refresh_life_time=40) == config.JWT_CONFIG\n        assert [User, Book, Author] == config.MODELS\n        assert {'dummy/': DummyAPI, 'ws/': DummyWS} == config.FLAT_URLS\n        assert {'dummy': DummyAPI, 'ws': DummyWS} == config.URLS\n        assert isinstance(config.WEBSOCKET_CONNECTIONS, WebsocketConnections)\n        assert config.BACKGROUND_TASKS is True\n        assert config.HAS_WS is True\n        assert config.TIMEZONE == 'Asia/Tehran'\n        assert config.TEMPLATES_DIR == 'templates/'\n        assert isinstance(config.JINJA_ENVIRONMENT, jinja2.environment.Environment)\n        assert config.AUTO_REFORMAT is True\n        assert config.QUERY_ENGINE is BasePantherDBQuery\n        assert isinstance(config.DATABASE, PantherDBConnection)\n\n    def test_loading_unknown_configs(self):\n        global CUSTOM_KEY\n        CUSTOM_KEY = 'I am custom'\n\n        # Check before loading configs.\n        assert config.CUSTOM_KEY is None\n\n        with self.assertNoLogs(level='ERROR'):\n            Panther(name=__name__, configs=__name__, urls={})\n\n        # Check after loading configs.\n        assert config.CUSTOM_KEY == 'I am custom'\n\n    def test_loading_unknown_configs_invalid_name(self):\n        global CUSTOM_key\n        CUSTOM_key = 'I am custom'\n\n        # Check before loading configs.\n        assert config.CUSTOM_key is None\n\n        with self.assertNoLogs(level='ERROR'):\n            Panther(name=__name__, configs=__name__, urls={})\n\n        # Check after loading configs.\n        assert config.CUSTOM_key is None\n\n    def test_refresh_configs(self):\n        global \\\n            BASE_DIR, \\\n            MONITORING, \\\n            LOG_QUERIES, \\\n            THROTTLING, \\\n            SECRET_KEY, \\\n            MIDDLEWARES, \\\n            HTTP_MIDDLEWARES, \\\n            WS_MIDDLEWARES, \\\n            USER_MODEL, \\\n            AUTHENTICATION, \\\n            WS_AUTHENTICATION, \\\n            JWT_CONFIG, \\\n            MODELS, \\\n            FLAT_URLS, \\\n            URLS, \\\n            WEBSOCKET_CONNECTIONS, \\\n            BACKGROUND_TASKS, \\\n            HAS_WS, \\\n            TIMEZONE, \\\n            TEMPLATES_DIR, \\\n            JINJA_ENVIRONMENT, \\\n            AUTO_REFORMAT, \\\n            QUERY_ENGINE, \\\n            DATABASE\n\n        # MIDDLEWARES\n        MIDDLEWARES = ['panther.middlewares.monitoring.MonitoringMiddleware']\n        WS_MIDDLEWARES = ['panther.middlewares.monitoring.WebsocketMonitoringMiddleware']\n        LOG_QUERIES = True\n        throttle = Throttle(rate=10, duration=timedelta(seconds=10))\n        THROTTLING = throttle\n        new_secret_key = generate_secret_key()\n        SECRET_KEY = new_secret_key\n        USER_MODEL = 'tests.test_config.User'\n        AUTHENTICATION = 'panther.authentications.QueryParamJWTAuthentication'\n        WS_AUTHENTICATION = 'panther.authentications.CookieJWTAuthentication'\n        JWT_CONFIG = {'life_time': timedelta(seconds=20)}\n        BACKGROUND_TASKS = True\n        TIMEZONE = 'Asia/Tehran'\n        TEMPLATES_DIR = 'templates/'\n        AUTO_REFORMAT = True\n        DATABASE = {\n            'engine': {\n                'class': 'panther.db.connections.PantherDBConnection',\n            },\n        }\n\n        with self.assertNoLogs(level='ERROR'):\n            Panther(name=__name__, configs=__name__, urls={'dummy': DummyAPI, 'ws': DummyWS})\n        config.refresh()\n\n        assert Path() == config.BASE_DIR\n        assert config.MONITORING is False\n        assert config.LOG_QUERIES is False\n        assert config.THROTTLING is None\n        assert config.SECRET_KEY is None\n        assert config.HTTP_MIDDLEWARES == []\n        assert config.WS_MIDDLEWARES == []\n        assert config.USER_MODEL is None\n        assert config.AUTHENTICATION is None\n        assert config.WS_AUTHENTICATION is None\n        assert config.JWT_CONFIG is None\n        assert config.MODELS == []\n        assert config.FLAT_URLS == {}\n        assert config.URLS == {}\n        assert config.WEBSOCKET_CONNECTIONS is None\n        assert config.BACKGROUND_TASKS is False\n        assert config.HAS_WS is False\n        assert config.TIMEZONE == 'UTC'\n        assert config.TEMPLATES_DIR == '.'\n        assert config.JINJA_ENVIRONMENT is None\n        assert config.AUTO_REFORMAT is False\n        assert config.QUERY_ENGINE is None\n        assert config.DATABASE is None\n\n    def test_loading_unknown_config_types(self):\n        global CUSTOM_INT, CUSTOM_LIST, CUSTOM_DICT, CUSTOM_BOOL\n        CUSTOM_INT = 5\n        CUSTOM_LIST = [1, 2]\n        CUSTOM_DICT = {'name': 'ali'}\n        CUSTOM_BOOL = True\n\n        # Check before loading configs.\n        assert config.CUSTOM_INT is None\n        assert config.CUSTOM_LIST is None\n        assert config.CUSTOM_DICT is None\n        assert config.CUSTOM_BOOL is None\n\n        with self.assertNoLogs(level='ERROR'):\n            Panther(name=__name__, configs=__name__, urls={})\n\n        # Check after loading configs.\n        assert config.CUSTOM_INT == 5\n        assert config.CUSTOM_LIST == [1, 2]\n        assert config.CUSTOM_DICT == {'name': 'ali'}\n        assert config.CUSTOM_BOOL is True\n"
  },
  {
    "path": "tests/test_cors.py",
    "content": "from unittest import IsolatedAsyncioTestCase\n\nfrom panther import Panther\nfrom panther.app import API\nfrom panther.configs import config\nfrom panther.test import APIClient\n\n\n@API()\nasync def simple_api():\n    return {'detail': 'ok'}\n\n\nurls = {'test': simple_api}\n\nALLOW_ORIGINS = ['*']\nALLOW_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']\nALLOW_HEADERS = ['*']\nALLOW_CREDENTIALS = False\nEXPOSE_HEADERS = []\nCORS_MAX_AGE = 600\nMIDDLEWARES = ['panther.middlewares.cors.CORSMiddleware']\n\n\nclass TestCORSMiddlewares(IsolatedAsyncioTestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        cls.app = Panther(__name__, configs=__name__, urls=urls)\n        cls.client = APIClient(app=cls.app)\n\n    @classmethod\n    def tearDownClass(cls) -> None:\n        config.refresh()\n\n    async def test_default_cors_headers(self):\n        allow_origins = config.ALLOW_ORIGINS\n        allow_methods = config.ALLOW_METHODS\n        allow_headers = config.ALLOW_HEADERS\n        allow_credentials = config.ALLOW_CREDENTIALS\n        expose_headers = config.EXPOSE_HEADERS\n        res = await self.client.get(path='test', headers={'origin': 'http://example.com'})\n        assert res.status_code == 200\n        assert res.headers['Access-Control-Allow-Origin'] == '*'\n        assert res.headers['Access-Control-Allow-Methods'] == ', '.join(ALLOW_METHODS)\n        assert res.headers['Access-Control-Allow-Headers'] == ', '.join(ALLOW_HEADERS)\n        assert res.headers['Access-Control-Max-Age'] == str(CORS_MAX_AGE)\n        assert 'Access-Control-Allow-Credentials' not in res.headers\n        assert 'Access-Control-Expose-Headers' not in res.headers\n        config.ALLOW_ORIGINS = allow_origins\n        config.ALLOW_METHODS = allow_methods\n        config.ALLOW_HEADERS = allow_headers\n        config.ALLOW_CREDENTIALS = allow_credentials\n        config.EXPOSE_HEADERS = expose_headers\n\n    async def test_preflight_options(self):\n        res = await self.client.options(path='test', headers={'origin': 'http://example.com'})\n        assert res.status_code == 204\n        assert res.headers['Access-Control-Allow-Origin'] == '*'\n        assert res.headers['Access-Control-Allow-Methods'] == ', '.join(ALLOW_METHODS)\n        assert res.headers['Access-Control-Allow-Headers'] == ', '.join(ALLOW_HEADERS)\n        assert res.headers['Access-Control-Max-Age'] == str(CORS_MAX_AGE)\n\n    async def test_custom_origins(self):\n        allow_origins = config.ALLOW_ORIGINS\n        config.ALLOW_ORIGINS = ['https://allowed.com']\n        res = await self.client.get(path='test', headers={'origin': 'https://allowed.com'})\n        assert res.headers['Access-Control-Allow-Origin'] == 'https://allowed.com'\n        res = await self.client.get(path='test', headers={'origin': 'https://not-allowed.com'})\n        assert res.headers['Access-Control-Allow-Origin'] == 'https://allowed.com'\n        config.ALLOW_ORIGINS = allow_origins\n\n    async def test_credentials(self):\n        allow_credentials = config.ALLOW_CREDENTIALS\n        config.ALLOW_CREDENTIALS = True\n        res = await self.client.get(path='test', headers={'origin': 'http://example.com'})\n        assert res.headers['Access-Control-Allow-Credentials'] == 'true'\n        config.ALLOW_CREDENTIALS = allow_credentials\n\n    async def test_expose_headers(self):\n        expose_headers = config.EXPOSE_HEADERS\n        config.EXPOSE_HEADERS = ['X-My-Header', 'X-Another-Header']\n        res = await self.client.get(path='test', headers={'origin': 'http://example.com'})\n        assert res.headers['Access-Control-Expose-Headers'] == 'X-My-Header, X-Another-Header'\n        config.EXPOSE_HEADERS = expose_headers\n"
  },
  {
    "path": "tests/test_database.py",
    "content": "import random\nfrom pathlib import Path\nfrom unittest import IsolatedAsyncioTestCase\n\nimport faker\nimport pytest\nfrom pantherdb import Cursor as PantherDBCursor\nfrom pydantic import BaseModel\n\nfrom panther import Panther\nfrom panther.configs import config\nfrom panther.db import Model\nfrom panther.db.connections import db\nfrom panther.db.cursor import Cursor as MongoCursor\n\nf = faker.Faker()\n\n\nclass Book(Model):\n    name: str\n    author: str\n    pages_count: int\n\n\nclass Viewer(BaseModel):\n    first_name: str\n\n\nclass Library(Model):\n    name: str\n    books: list[Book]\n    viewer: Viewer\n\n\nclass _BaseDatabaseTestCase:\n    # # # Insert\n    async def test_insert_one(self):\n        name = f.name()\n        author = f.name()\n        pages_count = random.randint(0, 10)\n        book = await Book.insert_one(name=name, author=author, pages_count=pages_count)\n\n        assert isinstance(book, Book)\n        assert book.id\n        assert book.name == name\n        assert book.pages_count == pages_count\n\n    async def test_insert_many_with_insert_one(self):\n        insert_count = await self._insert_many()\n        assert insert_count > 1\n\n    async def test_insert_many(self):\n        insert_count = random.randint(2, 10)\n        initial_books_data = [\n            {'name': f.name(), 'author': f.name(), 'pages_count': random.randint(0, 10)} for _ in range(insert_count)\n        ]\n        books = await Book.insert_many(initial_books_data)\n        actual_books = [{'name': book.name, 'author': book.author, 'pages_count': book.pages_count} for book in books]\n        expected_books = [\n            {'name': book['name'], 'author': book['author'], 'pages_count': book['pages_count']}\n            for book in initial_books_data\n        ]\n        assert len(actual_books) == len(expected_books)\n        assert actual_books == expected_books\n\n    # # # FindOne\n    async def test_find_one_not_found(self):\n        # Insert Many\n        await self._insert_many()\n\n        # Find One\n        book = await Book.find_one(name='NotFound', author='NotFound', pages_count=0)\n\n        assert book is None\n\n    async def test_find_one_in_many_when_its_last(self):\n        # Insert Many\n        await self._insert_many()\n\n        # Insert One\n        name = f.name()\n        author = f.name()\n        pages_count = random.randint(0, 10)\n        created_book = await Book.insert_one(name=name, author=author, pages_count=pages_count)\n\n        # Find One\n        book = await Book.find_one(name=name, author=author, pages_count=pages_count)\n\n        assert isinstance(book, Book)\n        assert book.id\n        assert str(book._id) == str(book.id)\n        assert book.name == name\n        assert book.pages_count == pages_count\n        assert created_book == book\n\n    async def test_find_one_in_many_when_its_middle(self):\n        # Insert Many\n        await self._insert_many()\n\n        # Insert One\n        name = f.name()\n        author = f.name()\n        pages_count = random.randint(0, 10)\n        created_book = await Book.insert_one(name=name, author=author, pages_count=pages_count)\n\n        # Insert Many\n        await self._insert_many()\n\n        # Find One\n        book = await Book.find_one(name=name, author=author, pages_count=pages_count)\n\n        assert isinstance(book, Book)\n        assert book.id\n        assert book.name == name\n        assert book.pages_count == pages_count\n        assert created_book == book\n\n    async def test_first(self):\n        # Insert Many\n        await self._insert_many()\n\n        # Insert Many With Same Params\n        name = f.name()\n        author = f.name()\n        pages_count = random.randint(0, 10)\n        await self._insert_many_with_specific_params(name=name, author=author, pages_count=pages_count)\n\n        # Find First\n        book = await Book.first(name=name, author=author, pages_count=pages_count)\n\n        assert isinstance(book, Book)\n        assert book.id\n        assert book.name == name\n        assert book.pages_count == pages_count\n\n    async def test_first_not_found(self):\n        # Insert Many\n        await self._insert_many()\n\n        # Find First\n        book = await Book.first(name='NotFound', author='NotFound', pages_count=0)\n\n        assert book is None\n\n    async def test_last(self):\n        # Insert Many\n        await self._insert_many()\n\n        # Insert Many With Same Params\n        name = f.name()\n        author = f.name()\n        pages_count = random.randint(0, 10)\n        await self._insert_many_with_specific_params(name=name, author=author, pages_count=pages_count)\n        last_obj = await Book.insert_one(name=name, author=author, pages_count=pages_count)\n\n        # Find One\n        book = await Book.last(name=name, author=author, pages_count=pages_count)\n\n        assert isinstance(book, Book)\n        assert book.id == last_obj.id\n        assert book.name == name\n        assert book.pages_count == pages_count\n\n    async def test_last_not_found(self):\n        await self._insert_many()\n\n        # Find Last\n        book = await Book.last(name='NotFound', author='NotFound', pages_count=0)\n\n        assert book is None\n\n    # # # Find\n    async def test_find(self):\n        # Insert Many\n        await self._insert_many()\n\n        # Insert Many With Specific Name\n        name = f.name()\n        insert_count = await self._insert_many_with_specific_params(name=name)\n\n        # Find\n        books = await Book.find(name=name)\n        _len = sum(1 for _ in books)\n\n        if self.__class__.__name__ == 'TestMongoDB':\n            assert isinstance(books, MongoCursor)\n        else:\n            assert isinstance(books, PantherDBCursor)\n        assert _len == insert_count\n        for book in books:\n            assert isinstance(book, Book)\n            assert book.name == name\n\n    async def test_find_not_found(self):\n        # Insert Many\n        await self._insert_many()\n\n        # Find\n        books = await Book.find(name='NotFound')\n        _len = sum(1 for _ in books)\n\n        if self.__class__.__name__ == 'TestMongoDB':\n            assert isinstance(books, MongoCursor)\n        else:\n            assert isinstance(books, PantherDBCursor)\n        assert _len == 0\n\n    async def test_find_without_filter(self):\n        # Insert Many\n        insert_count = await self._insert_many()\n\n        # Find All\n        books = await Book.find()\n        _len = sum(1 for _ in books)\n\n        if self.__class__.__name__ == 'TestMongoDB':\n            assert isinstance(books, MongoCursor)\n        else:\n            assert isinstance(books, PantherDBCursor)\n        assert _len == insert_count\n        for book in books:\n            assert isinstance(book, Book)\n\n    async def test_all(self):\n        # Insert Many\n        insert_count = await self._insert_many()\n\n        # Find All\n        books = await Book.all()\n        _len = sum(1 for _ in books)\n\n        if self.__class__.__name__ == 'TestMongoDB':\n            assert isinstance(books, MongoCursor)\n        else:\n            assert isinstance(books, PantherDBCursor)\n\n        assert _len == insert_count\n        for book in books:\n            assert isinstance(book, Book)\n\n    async def test_aggregation(self):\n        # Insert Many\n        insert_count = await self._insert_many()\n\n        # Find All with aggregate\n        books = await Book.aggregate([])\n        _len = sum(1 for _ in books)\n\n        assert isinstance(books, list)\n\n        assert _len == insert_count\n        for book in books:\n            assert isinstance(book, dict)\n            assert {*book.keys()} == {'_id', 'name', 'author', 'pages_count'}\n\n    # # # Count\n    async def test_count_all(self):\n        # Insert Many\n        insert_count = await self._insert_many()\n\n        # Count All\n        books_count = await Book.count()\n\n        assert isinstance(books_count, int)\n        assert books_count == insert_count\n\n    async def test_count_with_filter(self):\n        # Insert Many\n        await self._insert_many()\n\n        # Insert Many With Specific Name\n        name = f.name()\n        insert_count = await self._insert_many_with_specific_params(name=name)\n\n        # Count\n        books_count = await Book.count(name=name)\n\n        assert isinstance(books_count, int)\n        assert books_count == insert_count\n\n    async def test_count_not_found(self):\n        # Insert Many\n        await self._insert_many()\n\n        # Count\n        books_count = await Book.count(name='NotFound')\n\n        assert isinstance(books_count, int)\n        assert books_count == 0\n\n    # # # Delete One\n    async def test_delete_one(self):\n        # Insert Many\n        await self._insert_many()\n\n        # Insert With Specific Name\n        name = f.name()\n        insert_count = await self._insert_many_with_specific_params(name=name)\n\n        # Delete One\n        is_deleted = await Book.delete_one(name=name)\n\n        assert isinstance(is_deleted, bool)\n        assert is_deleted is True\n\n        # Count Them After Deletion\n        assert await Book.count(name=name) == insert_count - 1\n\n    async def test_delete_self(self):\n        # Insert Many\n        await self._insert_many()\n\n        # Insert With Specific Name\n        name = f.name()\n        insert_count = await self._insert_many_with_specific_params(name=name)\n        # Delete One\n        book = await Book.find_one(name=name)\n        await book.delete()\n        # Count Them After Deletion\n        assert await Book.count(name=name) == insert_count - 1\n\n    async def test_delete_one_not_found(self):\n        # Insert Many\n        insert_count = await self._insert_many()\n\n        # Delete One\n        is_deleted = await Book.delete_one(name='_Invalid_Name_')\n\n        assert isinstance(is_deleted, bool)\n        assert is_deleted is False\n\n        # Count All\n        assert await Book.count() == insert_count\n\n    # # # Delete Many\n    async def test_delete_many(self):\n        # Insert Many\n        pre_insert_count = await self._insert_many()\n\n        # Insert With Specific Name\n        name = f.name()\n        insert_count = await self._insert_many_with_specific_params(name=name)\n\n        # Delete Many\n        deleted_count = await Book.delete_many(name=name)\n\n        assert isinstance(deleted_count, int)\n        assert deleted_count == insert_count\n\n        # Count Them After Deletion\n        assert await Book.count(name=name) == 0\n        assert await Book.count() == pre_insert_count\n\n    async def test_delete_many_not_found(self):\n        # Insert Many\n        pre_insert_count = await self._insert_many()\n\n        # Delete Many\n        name = 'NotFound'\n        deleted_count = await Book.delete_many(name=name)\n\n        assert isinstance(deleted_count, int)\n        assert deleted_count == 0\n\n        # Count Them After Deletion\n        assert await Book.count(name=name) == 0\n        assert await Book.count() == pre_insert_count\n\n    # # # Update\n    async def test_update_one(self):\n        # Insert Many\n        insert_count = await self._insert_many()\n\n        # Insert With Specific Name\n        name = f.name()\n        author = f.name()\n        pages_count = random.randint(0, 10)\n        await Book.insert_one(name=name, author=author, pages_count=pages_count)\n\n        # Update One\n        new_name = 'New Name'\n        is_updated = await Book.update_one({'name': name}, name=new_name)\n\n        assert isinstance(is_updated, bool)\n        assert is_updated is True\n\n        book = await Book.find_one(name=new_name)\n        assert isinstance(book, Book)\n        assert book.author == author\n        assert book.pages_count == pages_count\n\n        # Count Them After Update\n        assert await Book.count(name=name) == 0\n        assert await Book.count() == insert_count + 1\n\n    async def test_update_self(self):\n        # Insert Many\n        insert_count = await self._insert_many()\n\n        # Insert With Specific Name\n        name = f.name()\n        author = f.name()\n        pages_count = random.randint(0, 10)\n        await Book.insert_one(name=name, author=author, pages_count=pages_count)\n\n        # Update One\n        book = await Book.find_one(name=name)\n        new_name = 'New Name'\n        await book.update(name=new_name)\n\n        assert book.name == new_name\n        assert book.author == author\n\n        book = await Book.find_one(name=new_name)\n        assert isinstance(book, Book)\n        assert book.author == author\n        assert book.pages_count == pages_count\n\n        # Count Them After Update\n        assert await Book.count(name=name) == 0\n        assert await Book.count() == insert_count + 1\n\n    async def test_update_one_not_found(self):\n        # Insert Many\n        insert_count = await self._insert_many()\n\n        # Update One\n        new_name = 'New Name'\n        is_updated = await Book.update_one({'name': 'NotFound'}, name=new_name)\n\n        assert isinstance(is_updated, bool)\n        assert is_updated is False\n\n        book = await Book.find_one(name=new_name)\n        assert book is None\n\n        # Count Them After Update\n        assert await Book.count() == insert_count\n\n    # # # Update Many\n    async def test_update_many(self):\n        # Insert Many\n        pre_insert_count = await self._insert_many()\n\n        # Insert With Specific Name\n        name = f.name()\n        author = f.name()\n        pages_count = random.randint(0, 10)\n        insert_count = await self._insert_many_with_specific_params(name=name, author=author, pages_count=pages_count)\n\n        # Update Many\n        new_name = 'New Name'\n        updated_count = await Book.update_many({'name': name}, name=new_name)\n\n        assert isinstance(updated_count, int)\n        assert updated_count == insert_count\n\n        books = await Book.find(name=new_name)\n        _len = sum(1 for _ in books)\n\n        if self.__class__.__name__ == 'TestMongoDB':\n            assert isinstance(books, MongoCursor)\n        else:\n            assert isinstance(books, PantherDBCursor)\n        assert _len == updated_count == insert_count\n        for book in books:\n            assert book.author == author\n            assert book.pages_count == pages_count\n\n        # Count Them After Update\n        assert await Book.count() == pre_insert_count + insert_count\n\n    async def test_update_many_not_found(self):\n        # Insert Many\n        insert_count = await self._insert_many()\n\n        # Update Many\n        new_name = 'New Name'\n        updated_count = await Book.update_many({'name': 'NotFound'}, name=new_name)\n\n        assert isinstance(updated_count, int)\n        assert updated_count == 0\n\n        book = await Book.find_one(name=new_name)\n        assert book is None\n\n        # Count Them After Update\n        assert await Book.count() == insert_count\n\n    async def test_save_update(self):\n        await self._insert_many()\n\n        # Insert With Specific Name\n        name = f.name()\n        author = f.name()\n        pages_count = random.randint(0, 10)\n        book = await Book.insert_one(name=name, author=author, pages_count=pages_count)\n\n        # Save\n        assert book.id\n        assert book.author == author\n        book.author = 'New'\n        await book.save()\n\n        # Make sure instance is updated\n        assert book.author == 'New'\n\n        # Make sure database is updated\n        book = await Book.find_one(name=name)\n        assert isinstance(book, Book)\n        assert book.author == 'New'\n        assert book.pages_count == pages_count\n\n    async def test_save_insert(self):\n        name = f.name()\n        author = f.name()\n        pages_count = random.randint(0, 10)\n\n        book = Book(name=name, author=author, pages_count=pages_count)\n        await book.save()\n\n        assert book.id\n        assert book.author == author\n        assert book.pages_count == pages_count\n\n    async def test_save_nested_models(self):\n        library = await Library.insert_one(\n            name='Name1', viewer=Viewer(first_name='Ali'), books=[Book(name='Book1', author='Author1', pages_count=1)]\n        )\n        assert library.id\n        assert library.name == 'Name1'\n        assert library.viewer.first_name == 'Ali'\n        assert library.books[0].name == 'Book1'\n        assert library.books[0].author == 'Author1'\n        assert library.books[0].pages_count == 1\n\n    @classmethod\n    async def _insert_many(cls) -> int:\n        insert_count = random.randint(2, 10)\n\n        for _ in range(insert_count):\n            await Book.insert_one(name=f.name(), author=f.name(), pages_count=random.randint(0, 10))\n\n        return insert_count\n\n    @classmethod\n    async def _insert_many_with_specific_params(\n        cls,\n        name: str = f.name(),\n        author: str = f.name(),\n        pages_count: int = random.randint(0, 10),\n    ) -> int:\n        insert_count = random.randint(2, 10)\n\n        for _ in range(insert_count):\n            await Book.insert_one(name=name, author=author, pages_count=pages_count)\n\n        return insert_count\n\n\nclass TestPantherDB(_BaseDatabaseTestCase, IsolatedAsyncioTestCase):\n    DB_PATH = 'test.pdb'\n\n    @classmethod\n    def setUpClass(cls) -> None:\n        global DATABASE\n        DATABASE = {'engine': {'class': 'panther.db.connections.PantherDBConnection', 'path': cls.DB_PATH}}\n        Panther(__name__, configs=__name__, urls={})\n\n    def tearDown(self) -> None:\n        db.session.collection('Book').drop()\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n        Path(cls.DB_PATH).unlink(missing_ok=True)\n\n    async def test_aggregation(self):\n        pass\n\n\n@pytest.mark.mongodb\nclass TestMongoDB(_BaseDatabaseTestCase, IsolatedAsyncioTestCase):\n    DB_NAME = 'test.pdb'\n\n    @classmethod\n    def setUpClass(cls) -> None:\n        global DATABASE\n        DATABASE = {\n            'engine': {\n                'class': 'panther.db.connections.MongoDBConnection',\n                'host': f'mongodb://127.0.0.1:27017/{cls.DB_NAME}',\n            },\n        }\n\n    def setUp(self):\n        Panther(__name__, configs=__name__, urls={})\n\n    def tearDown(self) -> None:\n        db.session.drop_collection('Book')\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n"
  },
  {
    "path": "tests/test_database_advance.py",
    "content": "import asyncio\nimport contextlib\nfrom pathlib import Path\nfrom typing import List\nfrom unittest import IsolatedAsyncioTestCase\n\nimport faker\nimport pytest\nfrom pydantic import BaseModel\n\nfrom panther import Panther\nfrom panther.configs import config\nfrom panther.db import Model\nfrom panther.db.connections import db\nfrom panther.exceptions import DatabaseError, NotFoundAPIError\n\nwith contextlib.suppress(ImportError):\n    # Only required if user wants to use mongodb\n    import bson\n\nf = faker.Faker()\n\n\nclass Book(Model):\n    name: str\n\n\nclass BookDetail(BaseModel):\n    detail: str\n    book: Book\n    more_books: list[list[Book]]\n\n\nclass Viewer(BaseModel):\n    first_name: str\n\n\nclass Library(Model):\n    name: str\n    books: list[Book]\n    viewer: Viewer\n\n\nclass TestBook(Model):\n    name: str\n    author: str\n    pages_count: int\n\n\nclass Author(Model):\n    name: str\n    books: list[Book]\n    books2: List[Book]\n    book: Book\n    book2: None | Book = None\n    book_detail: dict\n    our_book_detail: BookDetail\n\n\nclass InvalidModel(Model):\n    new_book: dict[str, Book]\n\n\nclass Publisher(Model):\n    name: str\n    rating: float\n    is_active: bool\n\n\nclass _BaseDatabaseTestCase:\n    async def test_insert_one(self):\n        book = await Book.insert_one(name='my_test')\n        author = await Author.insert_one(\n            {'name': 'ali'},\n            books=[book],\n            books2=[book.id, book.model_dump()],\n            book=Book(name='test_book1'),\n            book2=None,\n            book_detail={'book1': book},\n            our_book_detail=BookDetail(detail='ok', book=book, more_books=[[book, book]]),\n        )\n\n        assert isinstance(book, Book)\n        assert book.id\n        assert book.name == 'my_test'\n\n        assert author.name == 'ali'\n        assert isinstance(author.books, list)\n        assert len(author.books) == 1\n        assert author.books[0] == book\n        assert author.books[0]\n\n        assert isinstance(author.books2, list)\n        assert len(author.books2) == 2\n        assert author.books2[0] == book\n        assert author.books2[1] == book\n\n        assert isinstance(author.book, Book)\n        assert author.book.id\n        assert author.book.name == 'test_book1'\n\n        assert author.book2 is None\n\n        assert isinstance(author.book_detail, dict)\n        assert list(author.book_detail.keys()) == ['book1']\n        assert author.book_detail['book1'] == book.id  # Known Issue\n\n        assert isinstance(author.our_book_detail, BookDetail)\n        assert author.our_book_detail.detail == 'ok'\n        assert author.our_book_detail.book == book\n        assert isinstance(author.our_book_detail.more_books, list)\n        assert len(author.our_book_detail.more_books) == 1\n        assert isinstance(author.our_book_detail.more_books[0], list)\n        assert len(author.our_book_detail.more_books[0]) == 2\n        assert author.our_book_detail.more_books[0][0] == book\n        assert author.our_book_detail.more_books[0][1] == book\n\n    async def test_insert_one_with_model_obj(self):\n        book = await Book.insert_one(name='my_test')\n        author = await Author.insert_one(\n            Author(\n                name='ali',\n                books=[book],\n                books2=[book, book],\n                book=Book(name='test_book1'),\n                book2=None,\n                book_detail={'book1': book},\n                our_book_detail=BookDetail(detail='ok', book=book, more_books=[[book, book]]),\n            )\n        )\n\n        assert isinstance(book, Book)\n        assert book.id\n        assert book.name == 'my_test'\n\n        assert author.name == 'ali'\n        assert isinstance(author.books, list)\n        assert len(author.books) == 1\n        assert author.books[0] == book\n        assert author.books[0]\n\n        assert isinstance(author.books2, list)\n        assert len(author.books2) == 2\n        assert author.books2[0] == book\n        assert author.books2[1] == book\n\n        assert isinstance(author.book, Book)\n        assert author.book.id\n        assert author.book.name == 'test_book1'\n\n        assert author.book2 is None\n\n        assert isinstance(author.book_detail, dict)\n        assert list(author.book_detail.keys()) == ['book1']\n        assert author.book_detail['book1'] == book.id  # Known Issue\n\n        assert isinstance(author.our_book_detail, BookDetail)\n        assert author.our_book_detail.detail == 'ok'\n        assert author.our_book_detail.book == book\n        assert isinstance(author.our_book_detail.more_books, list)\n        assert len(author.our_book_detail.more_books) == 1\n        assert isinstance(author.our_book_detail.more_books[0], list)\n        assert len(author.our_book_detail.more_books[0]) == 2\n        assert author.our_book_detail.more_books[0][0] == book\n        assert author.our_book_detail.more_books[0][1] == book\n\n    async def test_insert_one_with_model_dump(self):\n        book = await Book.insert_one(name='my_test')\n        author = await Author.insert_one(\n            Author(\n                name='ali',\n                books=[book],\n                books2=[book, book],\n                book=book,\n                book2=None,\n                book_detail={'book1': book},\n                our_book_detail=BookDetail(detail='ok', book=book, more_books=[[book, book]]),\n            ).model_dump()\n        )\n\n        assert isinstance(book, Book)\n        assert book.id\n        assert book.name == 'my_test'\n\n        assert author.name == 'ali'\n        assert isinstance(author.books, list)\n        assert len(author.books) == 1\n        assert author.books[0] == book\n        assert author.books[0]\n\n        assert isinstance(author.books2, list)\n        assert len(author.books2) == 2\n        assert author.books2[0] == book\n        assert author.books2[1] == book\n\n        assert isinstance(author.book, Book)\n        assert author.book == book\n\n        assert author.book2 is None\n\n        assert isinstance(author.book_detail, dict)\n        assert list(author.book_detail.keys()) == ['book1']\n        assert author.book_detail['book1'] == book.model_dump()\n\n        assert isinstance(author.our_book_detail, BookDetail)\n        assert author.our_book_detail.detail == 'ok'\n        assert author.our_book_detail.book == book\n        assert isinstance(author.our_book_detail.more_books, list)\n        assert len(author.our_book_detail.more_books) == 1\n        assert isinstance(author.our_book_detail.more_books[0], list)\n        assert len(author.our_book_detail.more_books[0]) == 2\n        assert author.our_book_detail.more_books[0][0] == book\n        assert author.our_book_detail.more_books[0][1] == book\n\n    async def test_insert_many(self):\n        book = await Book.insert_one(name='my_test')\n        authors = await Author.insert_many(\n            [\n                {\n                    'name': 'ali',\n                    'books': [book],\n                    'books2': [book.id, book],\n                    'book': Book(name='test_book1'),\n                    'book2': None,\n                    'book_detail': {'book1': book.model_dump()},\n                    'our_book_detail': BookDetail(detail='ok', book=book, more_books=[[book, book]]),\n                },\n                {\n                    'name': 'ali',\n                    'books': [book],\n                    'books2': [book.id, book],\n                    'book': Book(name='test_book2'),\n                    'book2': None,\n                    'book_detail': {'book1': book.model_dump()},\n                    'our_book_detail': BookDetail(detail='ok', book=book, more_books=[[book, book]]),\n                },\n            ],\n        )\n\n        for author in authors:\n            assert isinstance(book, Book)\n            assert book.id\n            assert book.name == 'my_test'\n\n            assert author.name == 'ali'\n            assert isinstance(author.books, list)\n            assert len(author.books) == 1\n            assert author.books[0] == book\n            assert author.books[0]\n\n            assert isinstance(author.books2, list)\n            assert len(author.books2) == 2\n            assert author.books2[0] == book\n            assert author.books2[1] == book\n\n            assert isinstance(author.book, Book)\n            assert author.book.id\n            assert author.book.name in ['test_book1', 'test_book2']\n\n            assert author.book2 is None\n\n            assert isinstance(author.book_detail, dict)\n            assert list(author.book_detail.keys()) == ['book1']\n            assert author.book_detail['book1'] == book.model_dump()\n\n            assert isinstance(author.our_book_detail, BookDetail)\n            assert author.our_book_detail.detail == 'ok'\n            assert author.our_book_detail.book == book\n            assert isinstance(author.our_book_detail.more_books, list)\n            assert len(author.our_book_detail.more_books) == 1\n            assert isinstance(author.our_book_detail.more_books[0], list)\n            assert len(author.our_book_detail.more_books[0]) == 2\n            assert author.our_book_detail.more_books[0][0] == book\n            assert author.our_book_detail.more_books[0][1] == book\n\n    async def test_find_one(self):\n        book = await Book.insert_one(name='my_test')\n        await Author.insert_one(\n            name='ali',\n            books=[book],\n            books2=[book, book],\n            book=Book(name='test_book1'),\n            book2=None,\n            book_detail={'book1': book},\n            our_book_detail=BookDetail(detail='ok', book=book, more_books=[[book, book]]),\n        )\n        author = await Author.find_one(name='ali')\n\n        assert isinstance(book, Book)\n        assert book.id\n        assert book.name == 'my_test'\n\n        assert author.name == 'ali'\n        assert isinstance(author.books, list)\n        assert len(author.books) == 1\n        assert author.books[0] == book\n        assert author.books[0]\n\n        assert isinstance(author.books2, list)\n        assert len(author.books2) == 2\n        assert author.books2[0] == book\n        assert author.books2[1] == book\n\n        assert isinstance(author.book, Book)\n        assert author.book.id\n        assert author.book.name == 'test_book1'\n\n        assert author.book2 is None\n\n        assert isinstance(author.book_detail, dict)\n        assert list(author.book_detail.keys()) == ['book1']\n        assert author.book_detail['book1'] == book.id  # Known Issue\n\n        assert isinstance(author.our_book_detail, BookDetail)\n        assert author.our_book_detail.detail == 'ok'\n        assert author.our_book_detail.book == book\n        assert isinstance(author.our_book_detail.more_books, list)\n        assert len(author.our_book_detail.more_books) == 1\n        assert isinstance(author.our_book_detail.more_books[0], list)\n        assert len(author.our_book_detail.more_books[0]) == 2\n        assert author.our_book_detail.more_books[0][0] == book\n        assert author.our_book_detail.more_books[0][1] == book\n\n    async def test_find(self):\n        book = await Book.insert_one(name='my_test')\n        await Author.insert_one(\n            name='ali',\n            books=[book],\n            books2=[book, book],\n            book=Book(name='test_book1'),\n            book2=None,\n            book_detail={'book1': book},\n            our_book_detail=BookDetail(detail='ok', book=book, more_books=[[book, book]]),\n        )\n        _authors = await Author.find(name='ali')\n        authors = [i for i in _authors]\n        assert len(authors) == 1\n        author = authors[0]\n\n        assert isinstance(book, Book)\n        assert book.id\n        assert book.name == 'my_test'\n\n        assert author.name == 'ali'\n        assert isinstance(author.books, list)\n        assert len(author.books) == 1\n        assert author.books[0] == book\n        assert author.books[0]\n\n        assert isinstance(author.books2, list)\n        assert len(author.books2) == 2\n        assert author.books2[0] == book\n        assert author.books2[1] == book\n\n        assert isinstance(author.book, Book)\n        assert author.book.id\n        assert author.book.name == 'test_book1'\n\n        assert author.book2 is None\n\n        assert isinstance(author.book_detail, dict)\n        assert list(author.book_detail.keys()) == ['book1']\n        assert author.book_detail['book1'] == book.id  # Known Issue\n\n        assert isinstance(author.our_book_detail, BookDetail)\n        assert author.our_book_detail.detail == 'ok'\n        assert author.our_book_detail.book == book\n        assert isinstance(author.our_book_detail.more_books, list)\n        assert len(author.our_book_detail.more_books) == 1\n        assert isinstance(author.our_book_detail.more_books[0], list)\n        assert len(author.our_book_detail.more_books[0]) == 2\n        assert author.our_book_detail.more_books[0][0] == book\n        assert author.our_book_detail.more_books[0][1] == book\n\n    async def test_insert_one_invalid_model(self):\n        book = await Book.insert_one(name='my_test')\n        try:\n            await InvalidModel.insert_one(new_book={'book': book})\n        except DatabaseError as e:\n            assert (\n                e.args[0]\n                == 'Panther does not support dict[str, tests.test_database_advance.Book] as a field type for unwrapping.'\n            )\n\n    # New comprehensive test cases\n    async def test_find_one_or_insert_existing(self):\n        \"\"\"Test find_one_or_insert when document exists\"\"\"\n        book = await Book.insert_one(name='existing_book')\n\n        result, is_inserted = await Book.find_one_or_insert(name='existing_book')\n\n        assert not is_inserted\n        assert result == book\n        assert result.name == 'existing_book'\n\n    async def test_find_one_or_insert_new(self):\n        \"\"\"Test find_one_or_insert when document doesn't exist\"\"\"\n        result, is_inserted = await Book.find_one_or_insert(name='new_book')\n\n        assert is_inserted\n        assert isinstance(result, Book)\n        assert result.name == 'new_book'\n        assert result.id\n\n    async def test_find_one_or_raise_found(self):\n        \"\"\"Test find_one_or_raise when document exists\"\"\"\n        book = await Book.insert_one(name='existing_book')\n\n        result = await Book.find_one_or_raise(name='existing_book')\n\n        assert result == book\n        assert result.name == 'existing_book'\n\n    async def test_find_one_or_raise_not_found(self):\n        \"\"\"Test find_one_or_raise when document doesn't exist\"\"\"\n        try:\n            await Book.find_one_or_raise(name='non_existent_book')\n            assert False, 'Should have raised NotFoundAPIError'\n        except NotFoundAPIError as e:\n            assert e.detail == 'Book Does Not Exist'\n\n    async def test_exists_true(self):\n        \"\"\"Test exists when document exists\"\"\"\n        await Book.insert_one(name='existing_book')\n\n        result = await Book.exists(name='existing_book')\n\n        assert result is True\n\n    async def test_exists_false(self):\n        \"\"\"Test exists when document doesn't exist\"\"\"\n        result = await Book.exists(name='non_existent_book')\n\n        assert result is False\n\n    async def test_cursor_operations(self):\n        \"\"\"Test cursor operations: skip, limit, sort\"\"\"\n        # Insert multiple books\n        books = []\n        for i in range(10):\n            book = await Book.insert_one(name=f'book_{i}')\n            books.append(book)\n\n        # Test skip and limit\n        cursor = await Book.find()\n        cursor = cursor.skip(2).limit(3)\n        results = [book async for book in cursor]\n\n        assert len(results) == 3\n        assert results[0].name == 'book_2'\n        assert results[1].name == 'book_3'\n        assert results[2].name == 'book_4'\n\n    async def test_cursor_sort(self):\n        \"\"\"Test cursor sorting\"\"\"\n        # Insert books with different names\n        await Book.insert_one(name='zebra')\n        await Book.insert_one(name='apple')\n        await Book.insert_one(name='banana')\n\n        # Test ascending sort\n        cursor = await Book.find()\n        cursor = cursor.sort([('name', 1)])\n        results = [book async for book in cursor]\n\n        assert len(results) == 3\n        assert results[0].name == 'apple'\n        assert results[1].name == 'banana'\n        assert results[2].name == 'zebra'\n\n        # Test descending sort\n        cursor = await Book.find()\n        cursor = cursor.sort([('name', -1)])\n        results = [book async for book in cursor]\n\n        assert len(results) == 3\n        assert results[0].name == 'zebra'\n        assert results[1].name == 'banana'\n        assert results[2].name == 'apple'\n\n    async def test_update_with_complex_data(self):\n        \"\"\"Test update operations with complex nested data\"\"\"\n        book = await Book.insert_one(name='original_book')\n        author = await Author.insert_one(\n            name='original_author',\n            books=[book],\n            books2=[book],\n            book=book,\n            book2=None,\n            book_detail={'book1': book},\n            our_book_detail=BookDetail(detail='original', book=book, more_books=[[book]]),\n        )\n\n        # Update with new book\n        new_book = await Book.insert_one(name='new_book')\n\n        await Author.update_one(\n            {'name': 'original_author'},\n            name='updated_author',\n            books=[new_book],\n            book=new_book,\n            our_book_detail=BookDetail(detail='updated', book=new_book, more_books=[[new_book]]),\n        )\n\n        updated_author = await Author.find_one(name='updated_author')\n        assert updated_author.name == 'updated_author'\n        assert len(updated_author.books) == 1\n        assert updated_author.books[0] == new_book\n        assert updated_author.book == new_book\n        assert updated_author.our_book_detail.detail == 'updated'\n\n    async def test_bulk_operations(self):\n        \"\"\"Test bulk insert and update operations\"\"\"\n        # Bulk insert\n        books_data = [{'name': 'bulk_book'} for _ in range(5)]\n        books = await Book.insert_many(books_data)\n\n        assert len(books) == 5\n        for book in books:\n            assert book.name == 'bulk_book'\n\n        # Bulk update\n        updated_count = await Book.update_many({'name': 'bulk_book'}, name='updated_bulk_book')\n\n        assert updated_count == 5\n\n        # Verify updates\n        updated_books = await Book.find(name='updated_bulk_book')\n        count = sum(1 for _ in updated_books)\n        assert count == 5\n\n    async def test_empty_collection_operations(self):\n        \"\"\"Test operations on empty collections\"\"\"\n        # Test find on empty collection\n        books = await Book.find()\n        count = sum(1 for _ in books)\n        assert count == 0\n\n        # Test count on empty collection\n        count = await Book.count()\n        assert count == 0\n\n        # Test find_one on empty collection\n        book = await Book.find_one(name='any_name')\n        assert book is None\n\n        # Test first/last on empty collection\n        first_book = await Book.first()\n        assert first_book is None\n\n        last_book = await Book.last()\n        assert last_book is None\n\n    async def test_model_validation_errors(self):\n        \"\"\"Test handling of invalid data\"\"\"\n        # Test with missing required fields\n        try:\n            await Book.insert_one()  # Missing name field\n            assert False, 'Should have raised validation error'\n        except DatabaseError as e:\n            assert 'Book(name=\"Field required\")' == str(e)\n\n    async def test_complex_nested_queries(self):\n        \"\"\"Test complex nested queries with multiple conditions\"\"\"\n        # Create test data\n        book1 = await Book.insert_one(name='book1')\n        book2 = await Book.insert_one(name='book2')\n\n        author1 = await Author.insert_one(\n            name='author1',\n            books=[book1],\n            books2=[book1],\n            book=book1,\n            book_detail={'book1': book1},\n            our_book_detail=BookDetail(detail='detail1', book=book1, more_books=[[book1]]),\n        )\n\n        author2 = await Author.insert_one(\n            name='author2',\n            books=[book2],\n            books2=[book2],\n            book=book2,\n            book_detail={'book2': book2},\n            our_book_detail=BookDetail(detail='detail2', book=book2, more_books=[[book2]]),\n        )\n\n        # Complex query\n        authors = await Author.find(name='author1')\n        results = [author for author in authors]\n        assert len(results) == 1\n        assert results[0].name == 'author1'\n\n    async def test_save_operations(self):\n        \"\"\"Test save operations for both insert and update\"\"\"\n        # Test save for new object (insert)\n        new_book = Book(name='new_book_save')\n        await new_book.save()\n\n        assert new_book.id is not None\n        assert new_book.name == 'new_book_save'\n\n        # Verify it was actually saved\n        saved_book = await Book.find_one(name='new_book_save')\n        assert saved_book is not None\n        assert saved_book.id == new_book.id\n\n        # Test save for existing object (update)\n        new_book.name = 'updated_book_save'\n        await new_book.save()\n\n        # Verify update\n        updated_book = await Book.find_one(name='updated_book_save')\n        assert updated_book is not None\n        assert updated_book.id == new_book.id\n\n    async def test_concurrent_operations(self):\n        \"\"\"Test concurrent operations (basic simulation)\"\"\"\n        # Insert multiple books concurrently\n        import asyncio\n\n        async def insert_book(name):\n            return await Book.insert_one(name=name)\n\n        # Create multiple concurrent insertions\n        tasks = [insert_book(f'concurrent_book_{i}') for i in range(5)]\n        books = await asyncio.gather(*tasks)\n\n        assert len(books) == 5\n        for i, book in enumerate(books):\n            assert book.name == f'concurrent_book_{i}'\n            assert book.id is not None\n\n    async def test_large_dataset_operations(self):\n        \"\"\"Test operations with larger datasets\"\"\"\n        # Insert larger dataset\n        books_data = [{'name': f'large_book_{i}'} for i in range(100)]\n        books = await Book.insert_many(books_data)\n\n        assert len(books) == 100\n\n        # Test pagination with large dataset\n        cursor = await Book.find()\n        cursor = cursor.skip(50).limit(25)\n        results = [book async for book in cursor]\n\n        assert len(results) == 25\n        assert results[0].name == 'large_book_50'\n        assert results[-1].name == 'large_book_74'\n\n        # Test count on large dataset\n        count = await Book.count()\n        assert count >= 100\n\n    async def test_edge_cases(self):\n        \"\"\"Test various edge cases\"\"\"\n        # Test with empty string\n        book = await Book.insert_one(name='')\n        assert book.name == ''\n\n        # Test with very long name\n        long_name = 'a' * 1000\n        book = await Book.insert_one(name=long_name)\n        assert book.name == long_name\n\n        # Test with special characters\n        special_name = 'book_with_!@#$%^&*()_+-=[]{}|;:,.<>?'\n        book = await Book.insert_one(name=special_name)\n        assert book.name == special_name\n\n    async def test_relationship_integrity(self):\n        \"\"\"Test relationship integrity and cascading effects\"\"\"\n        # Create books and authors with relationships\n        book1 = await Book.insert_one(name='book1')\n        book2 = await Book.insert_one(name='book2')\n\n        author = await Author.insert_one(\n            name='test_author',\n            books=[book1, book2],\n            books2=[book1, book2],\n            book=book1,\n            book_detail={'book1': book1, 'book2': book2},\n            our_book_detail=BookDetail(detail='test', book=book1, more_books=[[book1, book2]]),\n        )\n\n        # Verify relationships are properly established\n        assert len(author.books) == 2\n        assert len(author.books2) == 2\n        assert author.book == book1\n        assert len(author.our_book_detail.more_books[0]) == 2\n\n    async def test_error_handling_edge_cases(self):\n        \"\"\"Test error handling for various edge cases\"\"\"\n        # Test with None values where not allowed\n        try:\n            await Book.insert_one(name=None)\n            assert False, 'Should have raised validation error'\n        except Exception:\n            pass  # Expected to fail\n\n        # Test with invalid data types\n        try:\n            await Book.insert_one(name=123)  # Should be string\n            assert False, 'Should have raised validation error'\n        except Exception:\n            pass  # Expected to fail\n\n    async def test_performance_optimizations(self):\n        \"\"\"Test performance-related operations\"\"\"\n        # Test batch operations\n        batch_size = 50\n        books_data = [{'name': f'batch_book_{i}'} for i in range(batch_size)]\n\n        start_time = asyncio.get_event_loop().time()\n        books = await Book.insert_many(books_data)\n        end_time = asyncio.get_event_loop().time()\n\n        assert len(books) == batch_size\n        # Basic performance check (should complete in reasonable time)\n        assert end_time - start_time < 0.1  # Should complete within 0.1 seconds\n\n    async def test_data_consistency(self):\n        \"\"\"Test data consistency across operations\"\"\"\n        # Insert book\n        book = await Book.insert_one(name='consistency_test')\n\n        # Verify it exists\n        found_book = await Book.find_one(name='consistency_test')\n        assert found_book == book\n\n        # Update it\n        await Book.update_one({'name': 'consistency_test'}, name='updated_consistency_test')\n\n        # Verify update\n        updated_book = await Book.find_one(name='updated_consistency_test')\n        assert updated_book is not None\n        assert updated_book.id == book.id\n\n        # Verify old name doesn't exist\n        old_book = await Book.find_one(name='consistency_test')\n        assert old_book is None\n\n    async def test_partial_update(self):\n        \"\"\"Test partial update (patch) only updates specified fields\"\"\"\n        # Insert a book with all fields\n        book = await TestBook.insert_one(name='partial_book', author='original_author', pages_count=123)\n        # Partial update: only update author\n        await TestBook.update_one({'id': book.id}, author='patched_author')\n        # Fetch and check\n        updated = await TestBook.find_one(id=book.id)\n        assert updated.author == 'patched_author'\n        assert updated.name == 'partial_book'\n        assert updated.pages_count == 123\n        # Partial update: only update pages_count\n        await TestBook.update_one({'id': book.id}, pages_count=456)\n        updated2 = await TestBook.find_one(id=book.id)\n        assert updated2.pages_count == 456\n        assert updated2.author == 'patched_author'\n        assert updated2.name == 'partial_book'\n\n    async def test_first(self):\n        \"\"\"Test getting the first document matching a filter\"\"\"\n        await TestBook.insert_one(name='first1', author='a', pages_count=1)\n        await TestBook.insert_one(name='first2', author='a', pages_count=2)\n        first = await TestBook.first(author='a')\n        assert first is not None\n        assert first.author == 'a'\n        assert first.name in ['first1', 'first2']\n\n    async def test_last(self):\n        \"\"\"Test getting the last document matching a filter\"\"\"\n        await TestBook.insert_one(name='last1', author='b', pages_count=1)\n        await TestBook.insert_one(name='last2', author='b', pages_count=2)\n        last = await TestBook.last(author='b')\n        assert last is not None\n        assert last.author == 'b'\n        assert last.name in ['last1', 'last2']\n\n    async def test_save_nested_models(self):\n        \"\"\"Test saving a model with nested BaseModel fields\"\"\"\n        viewer = Viewer(first_name='Ali')\n        book = await Book.insert_one(name='nested_book')\n        library = await Library.insert_one(name='Central', books=[book], viewer=viewer)\n        assert library.id\n        assert library.name == 'Central'\n        assert isinstance(library.books, list)\n        assert library.books[0] == book\n        assert isinstance(library.viewer, Viewer)\n        assert library.viewer.first_name == 'Ali'\n\n\nclass TestPantherDB(_BaseDatabaseTestCase, IsolatedAsyncioTestCase):\n    DB_PATH = 'test.pdb'\n\n    @classmethod\n    def setUpClass(cls) -> None:\n        global DATABASE\n        DATABASE = {\n            'engine': {'class': 'panther.db.connections.PantherDBConnection', 'path': cls.DB_PATH},\n        }\n        Panther(__name__, configs=__name__, urls={})\n\n    def tearDown(self) -> None:\n        db.session.collection('Book').drop()\n        db.session.collection('Author').drop()\n        db.session.collection('Publisher').drop()\n        db.session.collection('TestBook').drop()\n        db.session.collection('Library').drop()\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n        Path(cls.DB_PATH).unlink(missing_ok=True)\n\n\n@pytest.mark.mongodb\nclass TestMongoDB(_BaseDatabaseTestCase, IsolatedAsyncioTestCase):\n    DB_NAME = 'test.pdb'\n\n    @classmethod\n    def setUpClass(cls) -> None:\n        global DATABASE\n        DATABASE = {\n            'engine': {\n                'class': 'panther.db.connections.MongoDBConnection',\n                'host': f'mongodb://127.0.0.1:27017/{cls.DB_NAME}',\n            },\n        }\n\n    def setUp(self):\n        Panther(__name__, configs=__name__, urls={})\n\n    def tearDown(self) -> None:\n        db.session.drop_collection('Book')\n        db.session.drop_collection('Author')\n        db.session.drop_collection('Publisher')\n        db.session.drop_collection('TestBook')\n        db.session.drop_collection('Library')\n\n    @classmethod\n    def tearDownClass(cls) -> None:\n        config.refresh()\n\n    async def test_insert_one_raw_document(self):\n        book = await Book.insert_one(name='my_test')\n        author = await Author.insert_one(\n            name='ali',\n            books=[book.model_dump()],\n            books2=[book.id, book],\n            book=Book(name='test_book1'),\n            book2=None,\n            book_detail={'book1': book},\n            our_book_detail=BookDetail(detail='ok', book=book, more_books=[[book, book]]),\n        )\n\n        document = await db.session['Author'].find_one()\n        assert isinstance(document, dict)\n        assert list(document.keys()) == [\n            '_id',\n            'name',\n            'books',\n            'books2',\n            'book',\n            'book2',\n            'book_detail',\n            'our_book_detail',\n        ]\n\n        assert document['_id'] == author._id\n\n        assert document['name'] == 'ali'\n\n        assert isinstance(document['books'], list)\n        assert len(document['books']) == 1\n        assert isinstance(document['books'][0], bson.ObjectId)\n        assert document['books'][0] == book._id\n\n        assert isinstance(document['books2'], list)\n        assert len(document['books2']) == 2\n        assert isinstance(document['books2'][0], bson.ObjectId)\n        assert document['books2'][0] == book._id\n        assert isinstance(document['books2'][1], bson.ObjectId)\n        assert document['books2'][1] == book._id\n\n        assert isinstance(document['book'], bson.ObjectId)\n        assert document['book'] != book._id  # A new book\n\n        assert document['book2'] is None\n\n        assert isinstance(document['book_detail'], dict)\n        assert list(document['book_detail'].keys()) == ['book1']\n        assert isinstance(document['book_detail']['book1'], bson.ObjectId)\n        assert document['book_detail']['book1'] == book._id\n\n        assert isinstance(document['our_book_detail'], dict)\n        assert list(document['our_book_detail'].keys()) == ['detail', 'book', 'more_books']\n        assert document['our_book_detail']['detail'] == 'ok'\n        assert isinstance(document['our_book_detail']['book'], bson.ObjectId)\n        assert document['our_book_detail']['book'] == book._id\n        assert isinstance(document['our_book_detail']['more_books'], list)\n        assert len(document['our_book_detail']['more_books']) == 1\n        assert isinstance(document['our_book_detail']['more_books'][0], list)\n        assert len(document['our_book_detail']['more_books'][0]) == 2\n        assert isinstance(document['our_book_detail']['more_books'][0][0], bson.ObjectId)\n        assert document['our_book_detail']['more_books'][0][0] == book._id\n        assert isinstance(document['our_book_detail']['more_books'][0][1], bson.ObjectId)\n        assert document['our_book_detail']['more_books'][0][1] == book._id\n\n    async def test_aggregation_pipeline(self):\n        \"\"\"Test MongoDB aggregation pipeline\"\"\"\n        # Insert test data\n        publishers = [\n            {'name': 'Publisher A', 'rating': 4.5, 'is_active': True},\n            {'name': 'Publisher B', 'rating': 3.8, 'is_active': True},\n            {'name': 'Publisher C', 'rating': 4.2, 'is_active': False},\n            {'name': 'Publisher D', 'rating': 4.7, 'is_active': True},\n        ]\n\n        await Publisher.insert_many(publishers)\n\n        # Test aggregation pipeline\n        pipeline = [{'$match': {'is_active': True}}, {'$group': {'_id': None, 'avg_rating': {'$avg': '$rating'}}}]\n\n        results = await Publisher.aggregate(pipeline)\n        results_list = list(results)\n\n        assert len(results_list) == 1\n        assert 'avg_rating' in results_list[0]\n        # Should be average of 4.5, 3.8, and 4.7 (active publishers only)\n        expected_avg = (4.5 + 3.8 + 4.7) / 3\n        assert abs(results_list[0]['avg_rating'] - expected_avg) < 0.01\n\n    async def test_mongodb_specific_features(self):\n        \"\"\"Test MongoDB-specific features like ObjectId handling\"\"\"\n        # Test ObjectId conversion\n        book = await Book.insert_one(name='mongodb_test')\n\n        # Verify _id property works correctly\n        assert hasattr(book, '_id')\n        assert isinstance(book._id, bson.ObjectId)\n        assert str(book._id) == str(book.id)\n\n        # Test querying by ObjectId\n        found_book = await Book.find_one(id=book._id)\n        assert found_book == book\n\n        # Test querying by string ID\n        found_book_str = await Book.find_one(id=str(book._id))\n        assert found_book_str == book\n\n    async def test_mongodb_complex_queries(self):\n        \"\"\"Test complex MongoDB queries with operators\"\"\"\n        # Insert test data with various ratings\n        publishers = [\n            {'name': 'High Rated', 'rating': 4.8, 'is_active': True},\n            {'name': 'Medium Rated', 'rating': 3.5, 'is_active': True},\n            {'name': 'Low Rated', 'rating': 2.1, 'is_active': False},\n            {'name': 'Top Rated', 'rating': 4.9, 'is_active': True},\n            {'name': 'Top Rated', 'rating': 5.9, 'is_active': False},\n        ]\n\n        await Publisher.insert_many(publishers)\n\n        # Test complex query with multiple conditions\n        high_rated_active = await Publisher.find({'rating': {'$gte': 4.0}, 'is_active': True})\n        results = [pub for pub in high_rated_active]\n\n        assert len(results) == 2  # High Rated, Top Rated, and one more\n        for pub in results:\n            assert pub.rating >= 4.0\n            assert pub.is_active is True\n\n    async def test_mongodb_bulk_operations(self):\n        \"\"\"Test MongoDB-specific bulk operations\"\"\"\n        # Test bulk write operations\n        books_data = [{'name': f'mongodb_bulk_{i}'} for i in range(10)]\n        books = await Book.insert_many(books_data)\n\n        # Test bulk update with MongoDB operators\n        update_result = await Book.update_many(\n            {'name': {'$regex': 'mongodb_bulk_'}}, {'$set': {'name': 'updated_mongodb_bulk'}}\n        )\n\n        # Verify all documents were updated\n        updated_books = await Book.find(name='updated_mongodb_bulk')\n        count = sum(1 for _ in updated_books)\n        assert count == 10\n\n    async def test_mongodb_data_types(self):\n        \"\"\"Test MongoDB-specific data types and handling\"\"\"\n        # Test with various data types that MongoDB handles well\n        test_data = {\n            'name': 'data_type_test',\n            'rating': 4.5,\n            'is_active': True,\n            'tags': ['fiction', 'adventure'],\n            'metadata': {'pages': 300, 'language': 'English'},\n        }\n\n        # Create a test model for this\n        class TestModel(Model):\n            name: str\n            rating: float\n            is_active: bool\n            tags: list[str]\n            metadata: dict\n\n        publisher = await TestModel.insert_one(**test_data)\n\n        assert publisher.name == 'data_type_test'\n        assert publisher.rating == 4.5\n        assert publisher.is_active is True\n        assert publisher.tags == ['fiction', 'adventure']\n        assert publisher.metadata == {'pages': 300, 'language': 'English'}\n\n    async def test_mongodb_error_handling(self):\n        \"\"\"Test MongoDB-specific error handling\"\"\"\n        # Test with invalid ObjectId\n        try:\n            await Book.find_one(id='invalid_object_id')\n            # Should handle gracefully\n        except Exception as e:\n            # Should not crash the application\n            assert 'invalid' in str(e).lower() or 'objectid' in str(e).lower()\n"
  },
  {
    "path": "tests/test_events.py",
    "content": "import logging\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom panther.configs import config\nfrom panther.events import Event\n\nlogger = logging.getLogger('panther')\n\n\nclass TestEvents(IsolatedAsyncioTestCase):\n    def setUp(self):\n        Event.clear()\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n\n    async def test_async_startup(self):\n        assert len(Event._startups) == 0\n\n        async def startup_event():\n            logger.info('This Is Startup.')\n\n        Event.startup(startup_event)\n\n        assert len(Event._startups) == 1\n        assert Event._startups[0] == startup_event\n\n        with self.assertLogs(level='INFO') as capture:\n            await Event.run_startups()\n\n        assert len(capture.records) == 1\n        assert capture.records[0].getMessage() == 'This Is Startup.'\n\n    async def test_sync_startup(self):\n        assert len(Event._startups) == 0\n\n        def startup_event():\n            logger.info('This Is Startup.')\n\n        Event.startup(startup_event)\n\n        assert len(Event._startups) == 1\n        assert Event._startups[0] == startup_event\n\n        with self.assertLogs(level='INFO') as capture:\n            await Event.run_startups()\n\n        assert len(capture.records) == 1\n        assert capture.records[0].getMessage() == 'This Is Startup.'\n\n    async def test_startup(self):\n        assert len(Event._startups) == 0\n\n        def startup_event1():\n            logger.info('This Is Startup1.')\n\n        async def startup_event2():\n            logger.info('This Is Startup2.')\n\n        Event.startup(startup_event1)\n        Event.startup(startup_event2)\n\n        assert len(Event._startups) == 2\n        assert Event._startups[0] == startup_event1\n        assert Event._startups[1] == startup_event2\n\n        with self.assertLogs(level='INFO') as capture:\n            await Event.run_startups()\n\n        assert len(capture.records) == 2\n        assert capture.records[0].getMessage() == 'This Is Startup1.'\n        assert capture.records[1].getMessage() == 'This Is Startup2.'\n\n    async def test_sync_shutdown(self):\n        assert len(Event._shutdowns) == 0\n\n        def shutdown_event():\n            logger.info('This Is Shutdown.')\n\n        Event.shutdown(shutdown_event)\n\n        assert len(Event._shutdowns) == 1\n        assert Event._shutdowns[0] == shutdown_event\n\n        with self.assertLogs(level='INFO') as capture:\n            Event.run_shutdowns()\n\n        assert len(capture.records) == 1\n        assert capture.records[0].getMessage() == 'This Is Shutdown.'\n\n    async def shutdown_event(self):\n        logger.info('This Is Shutdown.')\n\n    def test_async_shutdown(self):\n        assert len(Event._shutdowns) == 0\n\n        Event.shutdown(self.shutdown_event)\n\n        assert len(Event._shutdowns) == 1\n        assert Event._shutdowns[0] == self.shutdown_event\n\n        with self.assertLogs(level='INFO') as capture:\n            Event.run_shutdowns()\n\n        assert len(capture.records) == 1\n        assert capture.records[0].getMessage() == 'This Is Shutdown.'\n\n    def test_shutdown(self):\n        assert len(Event._shutdowns) == 0\n\n        def shutdown_event_sync():\n            logger.info('This Is Sync Shutdown.')\n\n        Event.shutdown(self.shutdown_event)\n        Event.shutdown(shutdown_event_sync)\n\n        assert len(Event._shutdowns) == 2\n        assert Event._shutdowns[0] == self.shutdown_event\n        assert Event._shutdowns[1] == shutdown_event_sync\n\n        with self.assertLogs(level='INFO') as capture:\n            Event.run_shutdowns()\n\n        assert len(capture.records) == 2\n        assert capture.records[0].getMessage() == 'This Is Shutdown.'\n        assert capture.records[1].getMessage() == 'This Is Sync Shutdown.'\n\n    def test_singleton_pattern(self):\n        \"\"\"Test that Event class works as a singleton\"\"\"\n        event1 = Event()\n        event2 = Event()\n        assert event1 is event2\n\n        # Test that the same instance is used for storing events\n        def test_func():\n            pass\n\n        Event.startup(test_func)\n        assert len(Event._startups) == 1\n        assert Event._startups[0] == test_func\n"
  },
  {
    "path": "tests/test_file.py",
    "content": "import platform\nimport tempfile\nimport unittest\nfrom pathlib import Path\n\nfrom panther._utils import detect_mime_type\nfrom panther.file_handler import File\n\n\nclass TestFile(unittest.TestCase):\n    def test_file_init_with_bytes(self):\n        data = b'hello world'\n        f = File(file_name='test.txt', content_type='text/plain', file=data)\n        assert f.file_name == 'test.txt'\n        assert f.content_type == 'text/plain'\n        assert f.file == data\n        assert f.size == len(data)\n\n    def test_file_init_with_path(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            file_path = Path(tmp_dir_name) / 'foo.txt'\n            file_path.write_bytes(b'abc123')\n            f = File(file_name=str(file_path), content_type='text/plain')\n            assert f.file_name == str(file_path)\n            assert f.content_type == 'text/plain'\n            assert f.size == 6\n\n    def test_file_read_and_seek(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            file_path = Path(tmp_dir_name) / 'bar.txt'\n            file_path.write_bytes(b'abcdef')\n            f = File(file_name=str(file_path), content_type='text/plain')\n            with f:\n                assert f.read(3) == b'abc'\n                assert f.tell() == 3\n                f.seek(0)\n                assert f.read() == b'abcdef'\n\n    def test_file_write_in_memory(self):\n        f = File(file_name='baz.txt', content_type='text/plain', file=b'foo')\n        f.write(b'bar')\n        assert f.file == b'foobar'\n        assert f.size == 6\n\n    def test_file_write_on_disk_raises(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            file_path = Path(tmp_dir_name) / 'baz.txt'\n            file_path.write_bytes(b'foo')\n            f = File(file_name=str(file_path), content_type='text/plain')\n            with f:\n                try:\n                    f.write(b'bar')\n                    assert False, 'Expected IOError to be raised'\n                except IOError:\n                    pass\n\n    def test_file_save_overwrite(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            f = File(file_name='test.txt', content_type='text/plain', file=b'data')\n            save_path = Path(tmp_dir_name) / 'out.txt'\n            result_path = f.save(str(save_path), overwrite=True)\n            assert Path(result_path).read_bytes() == b'data'\n            # Overwrite again\n            f2 = File(file_name='test.txt', content_type='text/plain', file=b'new')\n            result_path2 = f2.save(str(save_path), overwrite=True)\n            assert Path(result_path2).read_bytes() == b'new'\n            assert result_path == result_path2\n\n    def test_file_save_no_overwrite(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            f = File(file_name='test.txt', content_type='text/plain', file=b'data')\n            save_path = Path(tmp_dir_name) / 'out.txt'\n            result_path1 = f.save(str(save_path), overwrite=False)\n            assert Path(result_path1).read_bytes() == b'data'\n            # Save again, should create a new file with timestamp\n            f2 = File(file_name='test.txt', content_type='text/plain', file=b'new')\n            result_path2 = f2.save(str(save_path), overwrite=False)\n            assert Path(result_path2).read_bytes() == b'new'\n            assert result_path1 != result_path2\n\n    def test_file_save_idempotent(self):\n        \"\"\"Test that save() method is idempotent - returns same path on multiple calls\"\"\"\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            f = File(file_name='test.txt', content_type='text/plain', file=b'data')\n            save_path = Path(tmp_dir_name) / 'out.txt'\n\n            # First call\n            result_path1 = f.save(str(save_path), overwrite=True)\n            assert Path(result_path1).read_bytes() == b'data'\n\n            # Second call - should return same path\n            result_path2 = f.save(str(save_path), overwrite=True)\n            assert result_path1 == result_path2\n\n            # Third call - should return same path\n            result_path3 = f.save(str(save_path), overwrite=True)\n            assert result_path1 == result_path3\n\n            # Verify file content is still the same\n            assert Path(result_path1).read_bytes() == b'data'\n\n    def test_file_save_idempotent_with_different_paths(self):\n        \"\"\"Test that idempotency works even when called with different paths\"\"\"\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            f = File(file_name='test.txt', content_type='text/plain', file=b'data')\n\n            # First call\n            result_path1 = f.save(Path(tmp_dir_name) / 'first.txt', overwrite=True)\n\n            # Second call with different path - should still return first path\n            result_path2 = f.save(Path(tmp_dir_name) / 'second.txt', overwrite=True)\n            assert result_path1 == result_path2\n\n            # Third call with no path - should still return first path\n            result_path3 = f.save(overwrite=True)\n            assert result_path1 == result_path3\n\n    def test_file_save_directory_path(self):\n        \"\"\"Test saving to directory path (ending with /)\"\"\"\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            f = File(file_name='document.pdf', content_type='application/pdf', file=b'pdf content')\n\n            # Save to directory\n            result_path = f.save(f'{tmp_dir_name}/uploads/')\n            expected_path = Path(tmp_dir_name) / 'uploads' / 'document.pdf'\n\n            assert result_path == str(expected_path)\n            assert expected_path.exists()\n            assert expected_path.read_bytes() == b'pdf content'\n\n    def test_file_save_nested_directory_path(self):\n        \"\"\"Test saving to nested directory path\"\"\"\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            f = File(file_name='image.jpg', content_type='image/jpeg', file=b'jpeg content')\n\n            # Save to nested directory\n            result_path = f.save(f'{tmp_dir_name}/uploads/images/')\n            expected_path = Path(tmp_dir_name) / 'uploads' / 'images' / 'image.jpg'\n\n            assert result_path == str(expected_path)\n            assert expected_path.exists()\n            assert expected_path.read_bytes() == b'jpeg content'\n\n    def test_file_save_directory_path_with_custom_filename(self):\n        \"\"\"Test saving to directory with custom filename\"\"\"\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            f = File(file_name='original.txt', content_type='text/plain', file=b'content')\n\n            # Save to directory with custom filename\n            result_path = f.save(f'{tmp_dir_name}/uploads/custom_name.txt')\n            expected_path = Path(tmp_dir_name) / 'uploads' / 'custom_name.txt'\n\n            assert result_path == str(expected_path)\n            assert expected_path.exists()\n            assert expected_path.read_bytes() == b'content'\n\n    def test_file_save_directory_creation(self):\n        \"\"\"Test that directories are created automatically\"\"\"\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            f = File(file_name='test.txt', content_type='text/plain', file=b'data')\n\n            # Try to save to non-existent directory\n            target_dir = Path(tmp_dir_name) / 'new' / 'nested' / 'directory'\n            result_path = f.save(str(target_dir / 'test.txt'), overwrite=True)\n\n            # Directory should be created\n            assert target_dir.exists()\n            assert Path(result_path).exists()\n\n    def test_file_save_directory_path_idempotent(self):\n        \"\"\"Test that directory path saving is also idempotent\"\"\"\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            f = File(file_name='test.txt', content_type='text/plain', file=b'data')\n\n            # First call\n            result_path1 = f.save(f'{tmp_dir_name}/uploads/')\n\n            # Second call - should return same path\n            result_path2 = f.save(f'{tmp_dir_name}/uploads/')\n            assert result_path1 == result_path2\n\n            # Third call with different directory - should still return first path\n            result_path3 = f.save(f'{tmp_dir_name}/different/')\n            assert result_path1 == result_path3\n\n    def test_file_save_without_path_uses_filename(self):\n        \"\"\"Test that save() without path uses the original filename\"\"\"\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            original_cwd = Path.cwd()\n            try:\n                # Change to temp directory for testing\n                import os\n\n                os.chdir(tmp_dir_name)\n\n                f = File(file_name='test.txt', content_type='text/plain', file=b'data')\n                result_path = f.save()\n\n                # Should save as 'test.txt' in current directory\n                expected_path = Path(tmp_dir_name) / 'test.txt'\n                assert Path(result_path).resolve() == expected_path.resolve()\n                assert Path(result_path).exists()\n\n            finally:\n                os.chdir(original_cwd)\n\n    def test_file_repr_and_str(self):\n        f = File(file_name='foo.txt', content_type='text/plain', file=b'bar')\n        s = str(f)\n        assert 'foo.txt' in s\n        assert 'text/plain' in s\n\n    def test_file_ensure_buffer_error(self):\n        f = File(file_name='foo.txt', content_type='text/plain')\n        f.file = None\n        f._file_path = None\n        f._buffer = None\n        try:\n            f._ensure_buffer()\n            assert False, 'Expected ValueError to be raised'\n        except ValueError:\n            pass\n\n\nclass TestDetectMimeType(unittest.TestCase):\n    def test_extension_based_detection(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            file_path = Path(tmp_dir_name) / 'foo.txt'\n            file_path.write_text('hello')\n            assert detect_mime_type(str(file_path)) == 'text/plain'\n\n    def test_png_magic_number(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            file_path = Path(tmp_dir_name) / 'img.png'\n            file_path.write_bytes(b'\\x89PNG\\r\\n\\x1a\\n' + b'rest')\n            assert detect_mime_type(str(file_path)) == 'image/png'\n\n    def test_pdf_magic_number(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            file_path = Path(tmp_dir_name) / 'doc.pdf'\n            file_path.write_bytes(b'%PDF-1.4 some pdf data')\n            assert detect_mime_type(str(file_path)) == 'application/pdf'\n\n    def test_zip_magic_number(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            file_path = Path(tmp_dir_name) / 'archive.zip'\n            file_path.write_bytes(b'PK\\x03\\x04rest')\n            if platform.system() == 'Windows':\n                assert detect_mime_type(str(file_path)) == 'application/x-zip-compressed'\n            else:\n                assert detect_mime_type(str(file_path)) == 'application/zip'\n\n    def test_jpeg_magic_number(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            file_path = Path(tmp_dir_name) / 'img.jpg'\n            file_path.write_bytes(b'\\xff\\xd8\\xffrest')\n            assert detect_mime_type(str(file_path)) == 'image/jpeg'\n\n    def test_gif_magic_number(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            file_path = Path(tmp_dir_name) / 'img.gif'\n            file_path.write_bytes(b'GIF89a rest')\n            assert detect_mime_type(str(file_path)) == 'image/gif'\n\n    def test_bmp_magic_number(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            file_path = Path(tmp_dir_name) / 'img.bmp'\n            file_path.write_bytes(b'BMrest')\n            assert detect_mime_type(str(file_path)) == 'image/bmp'\n\n    def test_ico_magic_number(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            file_path = Path(tmp_dir_name) / 'icon'\n            file_path.write_bytes(b'\\x00\\x00\\x01\\x00rest')\n            assert detect_mime_type(str(file_path)) == 'image/x-icon'\n\n    def test_tiff_magic_number(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            file_path = Path(tmp_dir_name) / 'img.tiff'\n            file_path.write_bytes(b'II*\\x00rest')\n            assert detect_mime_type(str(file_path)) == 'image/tiff'\n\n    def test_mp4_magic_number(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            file_path = Path(tmp_dir_name) / 'video.mp4'\n            file_path.write_bytes(b'\\x00\\x00\\x00\\x18ftyprest')\n            assert detect_mime_type(str(file_path)) == 'video/mp4'\n\n    def test_fallback_octet_stream(self):\n        with tempfile.TemporaryDirectory() as tmp_dir_name:\n            file_path = Path(tmp_dir_name) / 'unknown.bin'\n            file_path.write_bytes(b'randomdata123456')\n            assert detect_mime_type(str(file_path)) == 'application/octet-stream'\n"
  },
  {
    "path": "tests/test_find_one_or_insert_race.py",
    "content": "import asyncio\nfrom unittest import IsolatedAsyncioTestCase\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom panther import Panther\nfrom panther.configs import config\nfrom panther.db import Model\nfrom panther.db.connections import db\n\n\nclass RaceBook(Model):\n    name: str\n\n\n@pytest.mark.mongodb\nclass TestFindOneOrInsertRaceMongoDB(IsolatedAsyncioTestCase):\n    DB_NAME = 'test_find_one_or_insert_race'\n\n    @classmethod\n    def setUpClass(cls) -> None:\n        global DATABASE\n        DATABASE = {\n            'engine': {\n                'class': 'panther.db.connections.MongoDBConnection',\n                'host': f'mongodb://127.0.0.1:27017/{cls.DB_NAME}',\n            },\n        }\n\n    def setUp(self):\n        Panther(__name__, configs=__name__, urls={})\n\n    def tearDown(self) -> None:\n        db.session.drop_collection('RaceBook')\n\n    @classmethod\n    def tearDownClass(cls) -> None:\n        config.refresh()\n\n    async def test_find_one_or_insert_with_unique_index_returns_single_record_under_concurrency(self):\n        await db.session['RaceBook'].create_index('name', unique=True)\n\n        concurrent_calls = 8\n        target_name = 'one-book-only'\n        initial_find_calls = 0\n        release_initial_finds = asyncio.Event()\n        find_lock = asyncio.Lock()\n        original_find_one = RaceBook.find_one\n\n        async def controlled_find_one(cls, _filter=None, /, **kwargs):\n            nonlocal initial_find_calls\n\n            should_force_initial_none = False\n            async with find_lock:\n                if initial_find_calls < concurrent_calls:\n                    initial_find_calls += 1\n                    should_force_initial_none = True\n                    if initial_find_calls == concurrent_calls:\n                        release_initial_finds.set()\n\n            if should_force_initial_none:\n                await release_initial_finds.wait()\n                return None\n\n            return await original_find_one(_filter, **kwargs)\n\n        with patch.object(RaceBook, 'find_one', new=classmethod(controlled_find_one)):\n            results = await asyncio.gather(\n                *[RaceBook.find_one_or_insert(name=target_name) for _ in range(concurrent_calls)],\n            )\n\n        inserted_count = sum(1 for _, is_inserted in results if is_inserted)\n        returned_ids = {obj.id for obj, _ in results}\n\n        assert inserted_count == 1\n        assert len(returned_ids) == 1\n        assert await RaceBook.count(name=target_name) == 1\n"
  },
  {
    "path": "tests/test_generics.py",
    "content": "from pathlib import Path\nfrom unittest import IsolatedAsyncioTestCase\n\nimport pytest\n\nfrom panther import Panther\nfrom panther.configs import config\nfrom panther.db import Model\nfrom panther.db.connections import db\nfrom panther.generics import CreateAPI, DeleteAPI, ListAPI, RetrieveAPI, UpdateAPI\nfrom panther.pagination import Pagination\nfrom panther.request import Request\nfrom panther.serializer import ModelSerializer\nfrom panther.test import APIClient\n\n\nclass User(Model):\n    name: str\n\n\nclass Person(User):\n    age: int\n\n\nclass RetrieveAPITest(RetrieveAPI):\n    async def get_instance(self, request: Request, **kwargs) -> Model:\n        return await User.find_one(id=kwargs['id'])\n\n\nclass ListAPITest(ListAPI):\n    async def get_query(self, request: Request, **kwargs):\n        return await User.find()\n\n\nclass FullListAPITest(ListAPI):\n    sort_fields = ['name', 'age']\n    search_fields = ['name']\n    filter_fields = ['id', 'name', 'age']\n    pagination = Pagination\n\n    async def get_query(self, request: Request, **kwargs):\n        return await Person.find()\n\n\nclass UserSerializer(ModelSerializer):\n    class Config:\n        model = User\n        fields = '*'\n\n\nclass UpdateAPITest(UpdateAPI):\n    input_model = UserSerializer\n\n    async def get_instance(self, request: Request, **kwargs) -> Model:\n        return await User.find_one(id=kwargs['id'])\n\n\nclass CreateAPITest(CreateAPI):\n    input_model = UserSerializer\n\n\nclass DeleteAPITest(DeleteAPI):\n    async def get_instance(self, request: Request, **kwargs) -> Model:\n        return await User.find_one(id=kwargs['id'])\n\n\nurls = {\n    'retrieve/<id>': RetrieveAPITest,\n    'list': ListAPITest,\n    'full-list': FullListAPITest,\n    'update/<id>': UpdateAPITest,\n    'create': CreateAPITest,\n    'delete/<id>': DeleteAPITest,\n}\n\n\nclass _BaseGenericTestCases:\n    async def test_retrieve(self):\n        user = await User.insert_one(name='Ali')\n        res = await self.client.get(f'retrieve/{user.id}')\n        assert res.status_code == 200\n        assert res.data == {'id': str(user.id), 'name': user.name}\n\n    async def test_list(self):\n        users = await User.insert_many([{'name': 'Ali'}, {'name': 'Hamed'}])\n        res = await self.client.get('list')\n        assert res.status_code == 200\n        assert res.data == [{'id': str(u.id), 'name': u.name} for u in users]\n\n    async def test_list_features(self):\n        users = await Person.insert_many(\n            [\n                {'name': 'Ali', 'age': 0},\n                {'name': 'Ali', 'age': 1},\n                {'name': 'Saba', 'age': 0},\n                {'name': 'Saba', 'age': 1},\n            ],\n        )\n        res = await self.client.get('full-list')\n        assert res.status_code == 200\n        assert set(res.data.keys()) == {'results', 'count', 'previous', 'next'}\n\n        # Normal\n        response = [{'name': r['name'], 'age': r['age']} for r in res.data['results']]\n        assert response == [\n            {'name': 'Ali', 'age': 0},\n            {'name': 'Ali', 'age': 1},\n            {'name': 'Saba', 'age': 0},\n            {'name': 'Saba', 'age': 1},\n        ]\n\n        # Sort 1\n        res = await self.client.get('full-list', query_params={'sort': '-name'})\n        response = [{'name': r['name'], 'age': r['age']} for r in res.data['results']]\n        assert response == [\n            {'name': 'Saba', 'age': 0},\n            {'name': 'Saba', 'age': 1},\n            {'name': 'Ali', 'age': 0},\n            {'name': 'Ali', 'age': 1},\n        ]\n\n        # Sort 2\n        res = await self.client.get('full-list', query_params={'sort': '-name,-age'})\n        response = [{'name': r['name'], 'age': r['age']} for r in res.data['results']]\n        assert response == [\n            {'name': 'Saba', 'age': 1},\n            {'name': 'Saba', 'age': 0},\n            {'name': 'Ali', 'age': 1},\n            {'name': 'Ali', 'age': 0},\n        ]\n\n        # Sort 3\n        res = await self.client.get('full-list', query_params={'sort': 'name,-age'})\n        response = [{'name': r['name'], 'age': r['age']} for r in res.data['results']]\n        assert response == [\n            {'name': 'Ali', 'age': 1},\n            {'name': 'Ali', 'age': 0},\n            {'name': 'Saba', 'age': 1},\n            {'name': 'Saba', 'age': 0},\n        ]\n\n        # Filter 1\n        res = await self.client.get('full-list', query_params={'sort': 'name,-age', 'name': 'Ali'})\n        response = [{'name': r['name'], 'age': r['age']} for r in res.data['results']]\n        assert response == [\n            {'name': 'Ali', 'age': 1},\n            {'name': 'Ali', 'age': 0},\n        ]\n\n        # Filter 2\n        res = await self.client.get('full-list', query_params={'sort': 'name,-age', 'id': users[1].id})\n        response = [{'id': r['id'], 'name': r['name'], 'age': r['age']} for r in res.data['results']]\n        assert response == [{'id': str(users[1].id), 'name': 'Ali', 'age': 1}]\n\n        # Filter 3\n        res = await self.client.get('full-list', query_params={'sort': 'name,-age', 'name': 'Alex'})\n        response = [{'name': r['name'], 'age': r['age']} for r in res.data['results']]\n        assert response == []\n\n        # Search\n        res = await self.client.get('full-list', query_params={'sort': 'name,-age', 'search': 'Ali'})\n        response = [{'name': r['name'], 'age': r['age']} for r in res.data['results']]\n        assert response == [\n            {'name': 'Ali', 'age': 1},\n            {'name': 'Ali', 'age': 0},\n        ]\n\n        # Pagination 1\n        res = await self.client.get('full-list', query_params={'sort': 'name,-age'})\n        assert res.data['previous'] is None\n        assert res.data['next'] is None\n        assert res.data['count'] == 4\n\n        # Pagination 2\n        res = await self.client.get('full-list', query_params={'limit': 2})\n        assert res.data['previous'] is None\n        assert res.data['next'] == '?limit=2&skip=2'\n        assert res.data['count'] == 4\n        response = [{'name': r['name'], 'age': r['age']} for r in res.data['results']]\n        assert response == [\n            {'name': 'Ali', 'age': 0},\n            {'name': 'Ali', 'age': 1},\n        ]\n\n        res = await self.client.get('full-list', query_params={'limit': 2, 'skip': 2})\n        assert res.data['previous'] == '?limit=2&skip=0'\n        assert res.data['next'] is None\n        assert res.data['count'] == 4\n        response = [{'name': r['name'], 'age': r['age']} for r in res.data['results']]\n        assert response == [\n            {'name': 'Saba', 'age': 0},\n            {'name': 'Saba', 'age': 1},\n        ]\n\n    async def test_update(self):\n        users = await User.insert_many([{'name': 'Ali'}, {'name': 'Hamed'}])\n        res = await self.client.put(f'update/{users[1].id}', payload={'name': 'NewName'})\n        assert res.status_code == 200\n        assert res.data['name'] == 'NewName'\n\n        new_users = await User.find()\n        users[1].name = 'NewName'\n        assert {(u.id, u.name) for u in new_users} == {(u.id, u.name) for u in users}\n\n    async def test_create(self):\n        res = await self.client.post('create', payload={'name': 'Sara'})\n        assert res.status_code == 201\n        assert res.data['name'] == 'Sara'\n\n        new_users = list(await User.find())\n        assert len(new_users)\n        assert new_users[0].name == 'Sara'\n\n    async def test_delete(self):\n        users = await User.insert_many([{'name': 'Ali'}, {'name': 'Hamed'}])\n        res = await self.client.delete(f'delete/{users[1].id}')\n        assert res.status_code == 204\n        new_users = list(await User.find())\n        assert len(new_users) == 1\n        assert new_users[0].model_dump() == users[0].model_dump()\n\n\nclass TestPantherDBGeneric(_BaseGenericTestCases, IsolatedAsyncioTestCase):\n    DB_PATH = 'test.pdb'\n\n    @classmethod\n    def setUpClass(cls) -> None:\n        global DATABASE\n        DATABASE = {\n            'engine': {'class': 'panther.db.connections.PantherDBConnection', 'path': cls.DB_PATH},\n        }\n        app = Panther(__name__, configs=__name__, urls=urls)\n        cls.client = APIClient(app=app)\n\n    def tearDown(self) -> None:\n        db.session.collection('User').drop()\n        db.session.collection('Person').drop()\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n        Path(cls.DB_PATH).unlink(missing_ok=True)\n\n\n@pytest.mark.mongodb\nclass TestMongoDBGeneric(_BaseGenericTestCases, IsolatedAsyncioTestCase):\n    DB_NAME = 'test.pdb'\n\n    @classmethod\n    def setUpClass(cls) -> None:\n        global DATABASE\n        DATABASE = {\n            'engine': {\n                'class': 'panther.db.connections.MongoDBConnection',\n                'host': f'mongodb://127.0.0.1:27017/{cls.DB_NAME}',\n            },\n        }\n\n    def setUp(self):\n        app = Panther(__name__, configs=__name__, urls=urls)\n        self.client = APIClient(app=app)\n\n    def tearDown(self) -> None:\n        db.session.drop_collection('User')\n        db.session.drop_collection('Person')\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n"
  },
  {
    "path": "tests/test_generics_search_regex_escape.py",
    "content": "import multiprocessing as mp\nimport re\nimport time\nfrom typing import ClassVar\nfrom unittest import TestCase\n\nfrom panther.configs import config\nfrom panther.db.connections import MongoDBConnection\nfrom panther.generics import ListAPI\nfrom panther.request import Request\n\n\ndef _run_regex_search(pattern: str, text: str, repeats: int, done: mp.Queue):\n    start = time.perf_counter()\n    for _ in range(repeats):\n        re.search(pattern, text)\n    done.put(time.perf_counter() - start)\n\n\nclass ListAPISearchTest(ListAPI):\n    search_fields: ClassVar[list[str]] = ['name']\n\n    async def get_query(self, request: Request, **kwargs):\n        raise NotImplementedError\n\n\nclass TestListAPISearchRegexEscape(TestCase):\n    REDOS_PATTERN = r'([a-zA-Z]+)*$'\n    PAYLOAD_LENGTH_RANGE = range(24, 42)\n    ESCAPED_TIMEOUT_SECONDS = 3.0\n    RAW_STALL_TIMEOUT_SECONDS = 0.5\n\n    def setUp(self):\n        self.previous_database = config.DATABASE\n        config.DATABASE = object.__new__(MongoDBConnection)\n\n    def tearDown(self):\n        config.DATABASE = self.previous_database\n\n    def test_mongodb_search_pattern_escapes_user_input(self):\n        user_input = '(a+)+$'\n\n        protected_pattern = self._extract_mongodb_search_pattern(user_input)\n\n        assert protected_pattern == re.escape(user_input)\n\n    def test_raw_regex_stalls_on_near_miss_payload_but_escaped_pattern_completes(self):\n        raw_pattern = self.REDOS_PATTERN\n        escaped_pattern = self._extract_mongodb_search_pattern(raw_pattern)\n\n        trigger_length, attempts = self._find_redos_trigger_length(\n            raw_pattern=raw_pattern,\n            escaped_pattern=escaped_pattern,\n        )\n\n        assert trigger_length is not None, f'ReDoS probe did not find a trigger: {attempts}'\n\n        raw_matching_completed, _ = self._run_with_deadline(\n            pattern=raw_pattern,\n            text='A' * trigger_length,\n            repeats=5_000,\n            timeout_seconds=self.ESCAPED_TIMEOUT_SECONDS,\n        )\n        assert raw_matching_completed is True, f'ReDoS probe: {attempts}'\n\n    def _extract_mongodb_search_pattern(self, user_input: str) -> str:\n        query = ListAPISearchTest().process_search(query_params={'search': user_input})\n        return query['$or'][0]['name']['$regex']\n\n    def _find_redos_trigger_length(self, *, raw_pattern: str, escaped_pattern: str) -> tuple[int | None, list[dict]]:\n        attempts: list[dict] = []\n\n        for length in self.PAYLOAD_LENGTH_RANGE:\n            near_miss_payload = 'A' * length + '!'\n            raw_completed, raw_elapsed = self._run_with_deadline(\n                pattern=raw_pattern,\n                text=near_miss_payload,\n                repeats=1,\n                timeout_seconds=self.RAW_STALL_TIMEOUT_SECONDS,\n            )\n            escaped_completed, escaped_elapsed = self._run_with_deadline(\n                pattern=escaped_pattern,\n                text=near_miss_payload,\n                repeats=5_000,\n                timeout_seconds=self.ESCAPED_TIMEOUT_SECONDS,\n            )\n\n            attempts.append(\n                {\n                    'payload_length': length,\n                    'raw_completed': raw_completed,\n                    'raw_elapsed': raw_elapsed,\n                    'escaped_completed': escaped_completed,\n                    'escaped_elapsed': escaped_elapsed,\n                },\n            )\n\n            if raw_completed is False and escaped_completed is True:\n                return length, attempts\n\n        return None, attempts\n\n    @staticmethod\n    def _run_with_deadline(\n        *,\n        pattern: str,\n        text: str,\n        repeats: int,\n        timeout_seconds: float,\n    ) -> tuple[bool, float | None]:\n        done: mp.Queue = mp.Queue()\n        process = mp.Process(target=_run_regex_search, args=(pattern, text, repeats, done))\n        process.start()\n        process.join(timeout=timeout_seconds)\n\n        if process.is_alive():\n            process.terminate()\n            process.join()\n            return False, None\n\n        if process.exitcode != 0:\n            raise AssertionError(f'regex worker failed with exit code {process.exitcode}')\n\n        elapsed_seconds = done.get_nowait() if not done.empty() else None\n        return True, elapsed_seconds\n"
  },
  {
    "path": "tests/test_middlewares.py",
    "content": "from unittest import IsolatedAsyncioTestCase, TestCase\n\nfrom panther import Panther\nfrom panther.app import API\nfrom panther.base_websocket import Websocket\nfrom panther.configs import config\nfrom panther.middlewares.base import HTTPMiddleware, WebsocketMiddleware\nfrom panther.request import Request\nfrom panther.test import APIClient, WebsocketClient\nfrom panther.websocket import GenericWebsocket\n\n\nclass MyMiddleware(HTTPMiddleware):\n    async def __call__(self, request: Request):\n        request.middlewares = [*getattr(request, 'middlewares', []), 'MyMiddleware']\n        response = await self.dispatch(request=request)\n        response.data = [*response.data, 'MyMiddleware']\n        return response\n\n\nclass BeforeMiddleware1(HTTPMiddleware):\n    async def __call__(self, request: Request):\n        request.middlewares = [*getattr(request, 'middlewares', []), 'BeforeMiddleware1']\n        return await self.dispatch(request=request)\n\n\nclass BeforeMiddleware2(HTTPMiddleware):\n    async def __call__(self, request: Request):\n        request.middlewares = [*getattr(request, 'middlewares', []), 'BeforeMiddleware2']\n        return await self.dispatch(request=request)\n\n\nclass AfterMiddleware1(HTTPMiddleware):\n    async def __call__(self, request: Request):\n        response = await self.dispatch(request=request)\n        response.data = [*response.data, 'AfterMiddleware1']\n        return response\n\n\nclass AfterMiddleware2(HTTPMiddleware):\n    async def __call__(self, request: Request):\n        response = await self.dispatch(request=request)\n        response.data = [*response.data, 'AfterMiddleware2']\n        return response\n\n\nclass AfterMiddleware3(HTTPMiddleware):\n    async def __call__(self, request: Request):\n        response = await self.dispatch(request=request)\n        response.data = [*response.data, 'AfterMiddleware3']\n        return response\n\n\nclass MyWSMiddleware1(WebsocketMiddleware):\n    async def __call__(self, connection: Websocket):\n        connection.middlewares = [*getattr(connection, 'middlewares', []), 'MyWSMiddleware1']\n        return await self.dispatch(connection=connection)\n\n\nclass MyWSMiddleware2(WebsocketMiddleware):\n    async def __call__(self, connection: Websocket):\n        connection.middlewares = [*getattr(connection, 'middlewares', []), 'MyWSMiddleware2']\n        return await self.dispatch(connection=connection)\n\n\nclass PrivateMiddleware1(HTTPMiddleware):\n    async def __call__(self, request: Request):\n        request.middlewares = [*getattr(request, 'middlewares', []), 'PrivateMiddleware1']\n        response = await self.dispatch(request=request)\n        response.data = [*response.data, 'PrivateMiddleware1']\n        return response\n\n\nclass PrivateMiddleware2(HTTPMiddleware):\n    async def __call__(self, request: Request):\n        request.middlewares = [*getattr(request, 'middlewares', []), 'PrivateMiddleware2']\n        response = await self.dispatch(request=request)\n        response.data = [*response.data, 'PrivateMiddleware2']\n        return response\n\n\n@API()\nasync def handle_middlewares(request: Request):\n    states = ['FunctionCall']\n    if hasattr(request, 'middlewares'):\n        states = request.middlewares + states\n    return states\n\n\n@API(middlewares=[])\nasync def handle_private_empty_middlewares(request: Request):\n    states = ['FunctionCall']\n    if hasattr(request, 'middlewares'):\n        states = request.middlewares + states\n    return states\n\n\n@API(middlewares=[PrivateMiddleware1, PrivateMiddleware2])\nasync def handle_private_middlewares(request: Request):\n    states = ['FunctionCall']\n    if hasattr(request, 'middlewares'):\n        states = request.middlewares + states\n    return states\n\n\nclass WebsocketHandleMiddlewares(GenericWebsocket):\n    async def connect(self):\n        await self.accept()\n        states = ['WebsocketConnect']\n        if hasattr(self, 'middlewares'):\n            states = self.middlewares + states\n        await self.send(states)\n        await self.close()\n\n\nurls = {\n    '': handle_middlewares,\n    'private-empty': handle_private_empty_middlewares,\n    'private': handle_private_middlewares,\n    'websocket': WebsocketHandleMiddlewares,\n}\n\n\nclass TestMiddleware(IsolatedAsyncioTestCase):\n    def tearDown(self):\n        config.refresh()\n\n    async def test_before_base_middleware(self):\n        global MIDDLEWARES\n        MIDDLEWARES = [BeforeMiddleware1]\n        app = Panther(__name__, configs=__name__, urls=urls)\n        client = APIClient(app=app)\n        response = await client.get('')\n        assert response.status_code == 200\n        assert response.data == ['BeforeMiddleware1', 'FunctionCall']\n        MIDDLEWARES = []\n\n    async def test_after_base_middleware(self):\n        global MIDDLEWARES\n        MIDDLEWARES = [AfterMiddleware1]\n        app = Panther(__name__, configs=__name__, urls=urls)\n        client = APIClient(app=app)\n        response = await client.get('')\n        assert response.status_code == 200\n        assert response.data == ['FunctionCall', 'AfterMiddleware1']\n        MIDDLEWARES = []\n\n    async def test_a_normal_middleware(self):\n        global MIDDLEWARES\n        MIDDLEWARES = [MyMiddleware]\n        app = Panther(__name__, configs=__name__, urls=urls)\n        client = APIClient(app=app)\n        response = await client.get('')\n        assert response.status_code == 200\n        assert response.data == ['MyMiddleware', 'FunctionCall', 'MyMiddleware']\n\n    async def test_websocket_middleware_in_http(self):\n        global MIDDLEWARES\n        MIDDLEWARES = [MyWSMiddleware1]\n        with self.assertLogs(level='ERROR') as captured:\n            try:\n                Panther(name=__name__, configs=__name__, urls={})\n            except SystemExit:\n                assert True\n            else:\n                assert False\n            finally:\n                MIDDLEWARES = []\n\n        assert len(captured.records) == 1\n        assert (\n            captured.records[0].getMessage()\n            == \"Invalid 'MIDDLEWARES': <class 'tests.test_middlewares.MyWSMiddleware1'> is not a sub class of `HTTPMiddleware`\"\n        )\n\n    async def test_private_empty_middleware(self):\n        app = Panther(__name__, configs=__name__, urls=urls)\n        client = APIClient(app=app)\n        response = await client.get('private-empty')\n        assert response.status_code == 200\n        assert response.data == ['FunctionCall']\n\n    async def test_private_middleware(self):\n        app = Panther(__name__, configs=__name__, urls=urls)\n        client = APIClient(app=app)\n        response = await client.get('private')\n        assert response.status_code == 200\n        assert response.data == [\n            'PrivateMiddleware1',\n            'PrivateMiddleware2',\n            'FunctionCall',\n            'PrivateMiddleware2',\n            'PrivateMiddleware1',\n        ]\n\n    async def test_middlewares_order(self):\n        global MIDDLEWARES\n        MIDDLEWARES = [\n            BeforeMiddleware2,\n            AfterMiddleware3,\n            MyMiddleware,\n            BeforeMiddleware1,\n            AfterMiddleware1,\n            AfterMiddleware2,\n            MyMiddleware,\n        ]\n        app = Panther(__name__, configs=__name__, urls=urls)\n        client = APIClient(app=app)\n        response = await client.get('private')\n        assert response.status_code == 200\n        assert response.data == [\n            'BeforeMiddleware2',\n            'MyMiddleware',\n            'BeforeMiddleware1',\n            'MyMiddleware',\n            'PrivateMiddleware1',\n            'PrivateMiddleware2',\n            'FunctionCall',\n            'PrivateMiddleware2',\n            'PrivateMiddleware1',\n            'MyMiddleware',\n            'AfterMiddleware2',\n            'AfterMiddleware1',\n            'MyMiddleware',\n            'AfterMiddleware3',\n        ]\n        MIDDLEWARES = []\n\n\nclass TestWebsocketMiddleware(TestCase):\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n\n    def setUp(self):\n        config.HAS_WS = True\n\n    def tearDown(self):\n        config.refresh()\n\n    def test_websocket_middleware(self):\n        global WS_MIDDLEWARES\n        WS_MIDDLEWARES = [MyWSMiddleware1, MyWSMiddleware2]\n        app = Panther(__name__, configs=__name__, urls=urls)\n        ws = WebsocketClient(app=app)\n        responses = ws.connect('websocket')\n        assert responses[0]['type'] == 'websocket.accept'\n        assert responses[0]['subprotocol'] is None\n        assert responses[0]['headers'] == {}\n\n        assert responses[1]['type'] == 'websocket.send'\n        assert responses[1]['text'] == '[\"MyWSMiddleware1\",\"MyWSMiddleware2\",\"WebsocketConnect\"]'\n\n        assert responses[2]['type'] == 'websocket.close'\n        assert responses[2]['code'] == 1000\n        assert responses[2]['reason'] == ''\n        WS_MIDDLEWARES = []\n\n    def test_http_middleware_in_websocket(self):\n        global WS_MIDDLEWARES\n        WS_MIDDLEWARES = [MyWSMiddleware1, MyMiddleware, MyWSMiddleware2]\n        with self.assertLogs(level='ERROR') as captured:\n            try:\n                Panther(name=__name__, configs=__name__, urls={})\n            except SystemExit:\n                assert True\n            else:\n                assert False\n            finally:\n                WS_MIDDLEWARES = []\n\n        assert len(captured.records) == 1\n        assert (\n            captured.records[0].getMessage()\n            == \"Invalid 'WS_MIDDLEWARES': <class 'tests.test_middlewares.MyMiddleware'> is not a sub class of `WebsocketMiddleware`\"\n        )\n\n    def test_base_middleware(self):\n        global WS_MIDDLEWARES\n        WS_MIDDLEWARES = [MyWSMiddleware1, MyWSMiddleware2]\n        app = Panther(__name__, configs=__name__, urls=urls)\n        ws = WebsocketClient(app=app)\n        responses = ws.connect('websocket')\n        assert responses[0]['type'] == 'websocket.accept'\n        assert responses[0]['subprotocol'] is None\n        assert responses[0]['headers'] == {}\n\n        assert responses[1]['type'] == 'websocket.send'\n        assert responses[1]['text'] == '[\"MyWSMiddleware1\",\"MyWSMiddleware2\",\"WebsocketConnect\"]'\n\n        assert responses[2]['type'] == 'websocket.close'\n        assert responses[2]['code'] == 1000\n        assert responses[2]['reason'] == ''\n        WS_MIDDLEWARES = []\n"
  },
  {
    "path": "tests/test_multipart.py",
    "content": "from unittest import IsolatedAsyncioTestCase\n\nfrom panther import Panther\nfrom panther.app import API\nfrom panther.configs import config\nfrom panther.request import Request\nfrom panther.test import APIClient\n\n\n@API()\nasync def flat_multipart_api(request: Request):\n    return request.data\n\n\n@API()\nasync def single_file_multipart_api(request: Request):\n    return {\n        'file_name': request.data['file'].file_name,\n        'content_type': request.data['file'].content_type,\n        'file': request.data['file'].file.decode(),\n    }\n\n\n@API()\nasync def several_file_multipart_api(request: Request):\n    return {\n        'file1': {\n            'file_name': request.data['file1'].file_name,\n            'content_type': request.data['file1'].content_type,\n            'file': request.data['file1'].file.decode(),\n        },\n        'file2': {\n            'file_name': request.data['file2'].file_name,\n            'content_type': request.data['file2'].content_type,\n            'file': request.data['file2'].file.decode(),\n        },\n    }\n\n\n@API()\nasync def complex_multipart_api(request: Request):\n    return {\n        'name': request.data['name'],\n        'age': request.data['age'],\n        'file1': {\n            'file_name': request.data['file1'].file_name,\n            'content_type': request.data['file1'].content_type,\n            'file': request.data['file1'].file.decode(),\n        },\n        'file2': {\n            'file_name': request.data['file2'].file_name,\n            'content_type': request.data['file2'].content_type,\n            'file': request.data['file2'].file.decode(),\n        },\n    }\n\n\n@API()\nasync def multipart_api(request: Request):\n    return request.data\n\n\nurls = {\n    'flat_multipart': flat_multipart_api,\n    'single_file_multipart': single_file_multipart_api,\n    'several_file_multipart': several_file_multipart_api,\n    'complex_multipart': complex_multipart_api,\n    'multiline_multipart': multipart_api,\n}\n\n\nclass TestMultipart(IsolatedAsyncioTestCase):\n    CONTENT_TYPE_1 = 'multipart/form-data; boundary=--------------------------201301649688174364392792'\n    CONTENT_TYPE_2 = 'multipart/form-data; boundary=----geckoformboundaryc30219e1237602175b34337f41ace019'\n    FLAT_PAYLOAD = (\n        b'----------------------------201301649688174364392792\\r\\n'\n        b'Content-Disposition: form-data; name=\"name\"\\r\\n\\r\\n'\n        b'Ali Rn\\r\\n'\n        b'----------------------------201301649688174364392792\\r\\n'\n        b'Content-Disposition: form-data; name=\"age\"\\r\\n\\r\\n'\n        b'25\\r\\n'\n        b'----------------------------201301649688174364392792--\\r\\n'\n    )\n    SINGLE_FILE_PAYLOAD = (\n        b'----------------------------201301649688174364392792\\r\\n'\n        b'Content-Disposition: form-data; name=\"file\"; filename=\"hello_world.txt\"\\r\\n'\n        b'Content-Type: text/plain\\r\\n\\r\\n'\n        b'Hello World\\n\\r\\n'\n        b'----------------------------201301649688174364392792--\\r\\n'\n    )\n    SEVERAL_FILE_PAYLOAD = (\n        b'----------------------------201301649688174364392792\\r\\n'\n        b'Content-Disposition: form-data; name=\"file1\"; filename=\"hello_world1.txt\"\\r\\n'\n        b'Content-Type: text/plain\\r\\n\\r\\n'\n        b'Hello World1\\n\\r\\n'\n        b'----------------------------201301649688174364392792\\r\\n'\n        b'Content-Disposition: form-data; name=\"file2\"; filename=\"hello_world2.txt\"\\r\\n'\n        b'Content-Type: text/plain\\r\\n\\r\\n'\n        b'Hello World2\\n\\r\\n'\n        b'----------------------------201301649688174364392792--\\r\\n'\n    )\n    COMPLEX_PAYLOAD = (\n        b'----------------------------201301649688174364392792\\r\\n'\n        b'Content-Disposition: form-data; name=\"name\"\\r\\n\\r\\n'\n        b'Ali Rn\\r\\n'\n        b'----------------------------201301649688174364392792\\r\\n'\n        b'Content-Disposition: form-data; name=\"file1\"; filename=\"hello_world1.txt\"\\r\\n'\n        b'Content-Type: text/plain\\r\\n\\r\\n'\n        b'Hello World1\\n\\r\\n'\n        b'----------------------------201301649688174364392792\\r\\n'\n        b'Content-Disposition: form-data; name=\"file2\"; filename=\"hello_world2.txt\"\\r\\n'\n        b'Content-Type: text/plain\\r\\n\\r\\n'\n        b'Hello World2\\n\\r\\n'\n        b'----------------------------201301649688174364392792\\r\\n'\n        b'Content-Disposition: form-data; name=\"age\"\\r\\n\\r\\n'\n        b'25\\r\\n'\n        b'----------------------------201301649688174364392792--\\r\\n'\n    )\n    MULTI_LINE_PAYLOAD = (\n        b'------geckoformboundaryc30219e1237602175b34337f41ace019\\r\\n'\n        b'Content-Disposition: form-data; name=\"team\"\\r\\n\\r\\n'\n        b'SRE\\r\\n'\n        b'------geckoformboundaryc30219e1237602175b34337f41ace019\\r\\n'\n        b'Content-Disposition: form-data; name=\"phone\"\\r\\n\\r\\n'\n        b'09033333333\\r\\n'\n        b'------geckoformboundaryc30219e1237602175b34337f41ace019\\r\\n'\n        b'Content-Disposition: form-data; name=\"message\"\\r\\n\\r\\n'\n        b'My\\r\\nName\\r\\nIs\\r\\nAli\\r\\n\\r\\n'\n        b'------geckoformboundaryc30219e1237602175b34337f41ace019--\\r\\n'\n    )\n\n    @classmethod\n    def setUpClass(cls) -> None:\n        app = Panther(__name__, configs=__name__, urls=urls)\n        cls.client = APIClient(app=app)\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n\n    async def test_flat_multipart(self):\n        res = await self.client.post(\n            'flat_multipart',\n            content_type=self.CONTENT_TYPE_1,\n            payload=self.FLAT_PAYLOAD,\n        )\n        assert res.status_code == 200\n        assert res.data == {'name': 'Ali Rn', 'age': '25'}\n\n    async def test_single_file_multipart(self):\n        res = await self.client.post(\n            'single_file_multipart',\n            content_type=self.CONTENT_TYPE_1,\n            payload=self.SINGLE_FILE_PAYLOAD,\n        )\n\n        assert res.status_code == 200\n        assert res.data == {\n            'file_name': 'hello_world.txt',\n            'content_type': 'text/plain',\n            'file': 'Hello World\\n',\n        }\n\n    async def test_several_file_multipart(self):\n        res = await self.client.post(\n            'several_file_multipart',\n            content_type=self.CONTENT_TYPE_1,\n            payload=self.SEVERAL_FILE_PAYLOAD,\n        )\n\n        assert res.status_code == 200\n        assert res.data == {\n            'file1': {\n                'file_name': 'hello_world1.txt',\n                'content_type': 'text/plain',\n                'file': 'Hello World1\\n',\n            },\n            'file2': {\n                'file_name': 'hello_world2.txt',\n                'content_type': 'text/plain',\n                'file': 'Hello World2\\n',\n            },\n        }\n\n    async def test_complex_multipart(self):\n        res = await self.client.post(\n            'complex_multipart',\n            content_type=self.CONTENT_TYPE_1,\n            payload=self.COMPLEX_PAYLOAD,\n        )\n\n        assert res.status_code == 200\n        assert res.data == {\n            'name': 'Ali Rn',\n            'age': '25',\n            'file1': {\n                'file_name': 'hello_world1.txt',\n                'content_type': 'text/plain',\n                'file': 'Hello World1\\n',\n            },\n            'file2': {\n                'file_name': 'hello_world2.txt',\n                'content_type': 'text/plain',\n                'file': 'Hello World2\\n',\n            },\n        }\n\n    async def test_multiline_multipart(self):\n        res = await self.client.post(\n            'multiline_multipart',\n            content_type=self.CONTENT_TYPE_2,\n            payload=self.MULTI_LINE_PAYLOAD,\n        )\n        assert res.data == {'team': 'SRE', 'phone': '09033333333', 'message': 'My\\r\\nName\\r\\nIs\\r\\nAli\\r\\n'}\n"
  },
  {
    "path": "tests/test_openapi.py",
    "content": "from unittest import IsolatedAsyncioTestCase\n\nfrom pydantic import BaseModel\n\nfrom panther import Panther, status\nfrom panther.app import API, GenericAPI\nfrom panther.configs import config\nfrom panther.db import Model\nfrom panther.openapi.urls import url_routing\nfrom panther.openapi.utils import EndpointParser\nfrom panther.response import Response\nfrom panther.test import APIClient\n\n\n@API()\ndef my_api1():\n    return {'detail': 'ok'}\n\n\n@API()\ndef my_api2():\n    data = {'detail': 'ok'}\n    return data\n\n\n@API()\ndef my_api3():\n    my_data = {'detail': 'ok'}\n    return Response(data=my_data)\n\n\n@API()\nasync def my_api4():\n    return Response(data={'detail': 'ok'})\n\n\n@API()\nasync def my_api5():\n    return Response(status_code=201)\n\n\n@API()\nasync def my_api6():\n    return Response(status_code=status.HTTP_207_MULTI_STATUS)\n\n\n@API()\ndef my_api7():\n    my_status = status.HTTP_207_MULTI_STATUS\n    return Response(status_code=my_status)\n\n\n@API()\ndef my_api8():\n    my_status = 207\n    return Response(status_code=my_status)\n\n\n@API()\ndef my_api9():\n    return Response({'detail': 'ok'})\n\n\n@API()\ndef my_api10():\n    my_data = {'detail': 'ok'}\n    return Response(my_data)\n\n\n@API()\ndef my_api11():\n    my_data = {'detail': 'ok'}\n    return Response(my_data, status_code=207)\n\n\n@API()\ndef my_api12():\n    my_data = {'detail': 'ok'}\n    return Response(my_data, status_code=status.HTTP_207_MULTI_STATUS)\n\n\n@API()\ndef my_api13():\n    my_data = {'detail': 'ok'}\n    my_status = 207\n    return Response(my_data, status_code=my_status)\n\n\n@API()\ndef my_api14():\n    my_data = {'detail': 'ok'}\n    my_status = status.HTTP_207_MULTI_STATUS\n    return Response(my_data, status_code=my_status)\n\n\n@API()\ndef my_api15():\n    my_data = {'detail': 'ok'}\n    my_status = status.HTTP_207_MULTI_STATUS\n    return Response(my_data, my_status)\n\n\n@API()\ndef my_api16():\n    my_data = {'detail': 'ok'}\n    return Response(my_data, status.HTTP_207_MULTI_STATUS)\n\n\n@API()\ndef my_api17():\n    return Response({}, 207)\n\n\nclass API18(GenericAPI):\n    def get(self, *args, **kwargs):\n        return {'detail': 'ok'}\n\n    def post(self, *args, **kwargs):\n        my_data = {'detail': 'ok'}\n        return my_data\n\n    async def put(self, *args, **kwargs):\n        my_data = {'detail': 'ok'}\n        return Response(data=my_data)\n\n    def patch(self, *args, **kwargs):\n        my_data = {'detail': 'ok'}\n        return Response(data=my_data, status_code=201)\n\n    def delete(self, *args, **kwargs):\n        return Response({}, status.HTTP_204_NO_CONTENT)\n\n\n@API()\ndef my_api19():\n    my_response = ['1', '2']\n    return my_response\n\n\n@API()\ndef my_api20():\n    my_response = 'Hello World!'\n    return my_response\n\n\n@API()\ndef my_api21():\n    my_response = True\n    return my_response\n\n\n@API()\ndef my_api22():\n    my_response = (6, 9)\n    return my_response\n\n\n@API()\ndef my_api23():\n    my_response = Response(data={'detail': 'Hello'}, status_code=207)\n    return my_response\n\n\nclass CustomBaseModel(BaseModel):\n    name: str\n    children: list[str] | None = None\n\n\n@API()\ndef my_api24():\n    my_response = CustomBaseModel(name='Ali', children=['A', 'B', 'C'])\n    return my_response\n\n\nclass CustomModel(Model):\n    title: str\n\n\n@API()\ndef my_api25():\n    my_response = CustomModel(title='Book')\n    return my_response\n\n\n@API()\ndef my_api26():\n    return CustomBaseModel(name='Ali')\n\n\n@API()\ndef my_api27():\n    return CustomModel(title='Book')\n\n\nclass TestOpenAPI(IsolatedAsyncioTestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        app = Panther(__name__, configs=__name__, urls={'docs': url_routing})\n        cls.client = APIClient(app=app)\n\n    @classmethod\n    def tearDownClass(cls) -> None:\n        config.refresh()\n\n    # TODO: Uncomment and improve it at end of this pr\n\n    async def test_swagger(self):\n        response = await self.client.get('/docs/swagger/')\n        expected_response = \"\"\"<!doctype html>\n<html>\n  <head>\n    <title>Swagger UI</title>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui.css\" />\n  </head>\n  <body>\n    <div id=\"swagger-ui\"></div>\n    <script src=\"https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui-bundle.js\"></script>\n    <script>\n      SwaggerUIBundle({ spec: {\"components\": {\"schemas\": {}, \"securitySchemes\": {\"BearerAuth\": {\"bearerFormat\": \"JWT\", \"scheme\": \"bearer\", \"type\": \"http\"}}}, \"info\": {\"description\": \"Auto-generated OpenAPI documentation for Panther project.\", \"title\": \"Panther API\", \"version\": \"1.0.0\"}, \"openapi\": \"3.0.0\", \"paths\": {\"/docs/rapidoc/\": {}, \"/docs/redoc/\": {}, \"/docs/scalar/\": {}, \"/docs/spotlight/\": {}, \"/docs/swagger/\": {}}, \"security\": [{\"BearerAuth\": []}]}, dom_id: '#swagger-ui' });\n    </script>\n  </body>\n</html>\"\"\"\n        assert expected_response == response.data\n\n    async def test_scalar(self):\n        response = await self.client.get('/docs/scalar/')\n        expected_response = \"\"\"<!doctype html>\n<html>\n  <head>\n    <title>Scalar API Reference</title>\n    <meta charset=\"utf-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, initial-scale=1\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script src=\"https://cdn.jsdelivr.net/npm/@scalar/api-reference\"></script>\n    <script>\n      Scalar.createApiReference('#app', {\n        content: {\"components\": {\"schemas\": {}, \"securitySchemes\": {\"BearerAuth\": {\"bearerFormat\": \"JWT\", \"scheme\": \"bearer\", \"type\": \"http\"}}}, \"info\": {\"description\": \"Auto-generated OpenAPI documentation for Panther project.\", \"title\": \"Panther API\", \"version\": \"1.0.0\"}, \"openapi\": \"3.0.0\", \"paths\": {\"/docs/rapidoc/\": {}, \"/docs/redoc/\": {}, \"/docs/scalar/\": {}, \"/docs/spotlight/\": {}, \"/docs/swagger/\": {}}, \"security\": [{\"BearerAuth\": []}]}\n      })\n    </script>\n  </body>\n</html>\"\"\"\n        assert expected_response == response.data\n\n    async def test_redoc(self):\n        response = await self.client.get('/docs/redoc/')\n        expected_response = \"\"\"<!doctype html>\n<html>\n  <head>\n    <title>ReDoc API Reference</title>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        margin: 0;\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div id=\"redoc-container\"></div>\n    <script src=\"https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js\"></script>\n    <script>\n      Redoc.init({\"components\": {\"schemas\": {}, \"securitySchemes\": {\"BearerAuth\": {\"bearerFormat\": \"JWT\", \"scheme\": \"bearer\", \"type\": \"http\"}}}, \"info\": {\"description\": \"Auto-generated OpenAPI documentation for Panther project.\", \"title\": \"Panther API\", \"version\": \"1.0.0\"}, \"openapi\": \"3.0.0\", \"paths\": {\"/docs/rapidoc/\": {}, \"/docs/redoc/\": {}, \"/docs/scalar/\": {}, \"/docs/spotlight/\": {}, \"/docs/swagger/\": {}}, \"security\": [{\"BearerAuth\": []}]}, {}, document.getElementById(\"redoc-container\"));\n    </script>\n  </body>\n</html>\"\"\"\n        assert expected_response == response.data\n\n    async def test_rapidoc(self):\n        response = await self.client.get('/docs/rapidoc/')\n        expected_response = \"\"\"<!doctype html>\n<html>\n  <head>\n    <title>RapiDoc Inline JSON</title>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes\">\n    <script type=\"module\" src=\"https://unpkg.com/rapidoc/dist/rapidoc-min.js\"></script>\n  </head>\n  <body>\n    <rapi-doc id=\"thedoc\"\n      render-style = \"read\"\n      allow-try=\"false\"\n      allow-authentication = \"false\"\n    > </rapi-doc>\n    <script>\n      document.addEventListener('readystatechange', (event) => {\n        let docEl = document.getElementById(\"thedoc\");\n        docEl.loadSpec({\"components\": {\"schemas\": {}, \"securitySchemes\": {\"BearerAuth\": {\"bearerFormat\": \"JWT\", \"scheme\": \"bearer\", \"type\": \"http\"}}}, \"info\": {\"description\": \"Auto-generated OpenAPI documentation for Panther project.\", \"title\": \"Panther API\", \"version\": \"1.0.0\"}, \"openapi\": \"3.0.0\", \"paths\": {\"/docs/rapidoc/\": {}, \"/docs/redoc/\": {}, \"/docs/scalar/\": {}, \"/docs/spotlight/\": {}, \"/docs/swagger/\": {}}, \"security\": [{\"BearerAuth\": []}]});\n      })\n    </script>\n  </body>\n</html>\"\"\"\n        assert expected_response == response.data\n\n    async def test_spotlight(self):\n        response = await self.client.get('/docs/spotlight/')\n        expected_response = \"\"\"<!doctype html>\n<html>\n<head>\n  <title>Stoplight Elements with CSS</title>\n  <link rel=\"stylesheet\" href=\"https://unpkg.com/@stoplight/elements/styles.min.css\" />\n  <script type=\"module\" src=\"https://unpkg.com/@stoplight/elements/web-components.min.js\"></script>\n  <style>\n    body, html {\n      margin: 0; padding: 0; height: 100%;\n    }\n    elements-api {\n      display: block;\n      height: 100vh;\n    }\n  </style>\n</head>\n<body>\n  <elements-api id=\"api-viewer\" router=\"hash\"></elements-api>\n  <script>\n    customElements.whenDefined(\"elements-api\").then(() => {\n      document.getElementById(\"api-viewer\").apiDescriptionDocument = {\"components\": {\"schemas\": {}, \"securitySchemes\": {\"BearerAuth\": {\"bearerFormat\": \"JWT\", \"scheme\": \"bearer\", \"type\": \"http\"}}}, \"info\": {\"description\": \"Auto-generated OpenAPI documentation for Panther project.\", \"title\": \"Panther API\", \"version\": \"1.0.0\"}, \"openapi\": \"3.0.0\", \"paths\": {\"/docs/rapidoc/\": {}, \"/docs/redoc/\": {}, \"/docs/scalar/\": {}, \"/docs/spotlight/\": {}, \"/docs/swagger/\": {}}, \"security\": [{\"BearerAuth\": []}]};\n    });\n  </script>\n</body>\n</html>\"\"\"\n        assert expected_response == response.data\n\n    async def test_my_api1(self):\n        parsed = EndpointParser(my_api1, 'get')\n        assert parsed.status_code == 200\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api2(self):\n        parsed = EndpointParser(my_api2, 'get')\n        assert parsed.status_code == 200\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api3(self):\n        parsed = EndpointParser(my_api3, 'get')\n        assert parsed.status_code == 200\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api4(self):\n        parsed = EndpointParser(my_api4, 'get')\n        assert parsed.status_code == 200\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api5(self):\n        parsed = EndpointParser(my_api5, 'get')\n        assert parsed.status_code == 201\n        assert parsed.response_data == {}\n\n    async def test_my_api6(self):\n        parsed = EndpointParser(my_api6, 'get')\n        assert parsed.status_code == 207\n        assert parsed.response_data == {}\n\n    async def test_my_api7(self):\n        parsed = EndpointParser(my_api7, 'get')\n        assert parsed.status_code == 207\n        assert parsed.response_data == {}\n\n    async def test_my_api8(self):\n        parsed = EndpointParser(my_api8, 'get')\n        assert parsed.status_code == 207\n        assert parsed.response_data == {}\n\n    async def test_my_api9(self):\n        parsed = EndpointParser(my_api9, 'get')\n        assert parsed.status_code == 200\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api10(self):\n        parsed = EndpointParser(my_api10, 'get')\n        assert parsed.status_code == 200\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api11(self):\n        parsed = EndpointParser(my_api11, 'get')\n        assert parsed.status_code == 207\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api12(self):\n        parsed = EndpointParser(my_api12, 'get')\n        assert parsed.status_code == 207\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api13(self):\n        parsed = EndpointParser(my_api13, 'get')\n        assert parsed.status_code == 207\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api14(self):\n        parsed = EndpointParser(my_api14, 'get')\n        assert parsed.status_code == 207\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api15(self):\n        parsed = EndpointParser(my_api15, 'get')\n        assert parsed.status_code == 207\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api16(self):\n        parsed = EndpointParser(my_api16, 'get')\n        assert parsed.status_code == 207\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api17(self):\n        parsed = EndpointParser(my_api17, 'get')\n        assert parsed.status_code == 207\n        assert parsed.response_data == {}\n\n    async def test_my_api18_get(self):\n        parsed = EndpointParser(API18, 'get')\n        assert parsed.status_code == 200\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api18_post(self):\n        parsed = EndpointParser(API18, 'post')\n        assert parsed.status_code == 200\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api18_put(self):\n        parsed = EndpointParser(API18, 'put')\n        assert parsed.status_code == 200\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api18_patch(self):\n        parsed = EndpointParser(API18, 'patch')\n        assert parsed.status_code == 201\n        assert parsed.response_data == {'detail': 'ok'}\n\n    async def test_my_api18_delete(self):\n        parsed = EndpointParser(API18, 'delete')\n        assert parsed.status_code == 204\n        assert parsed.response_data == {}\n\n    async def test_my_api19(self):\n        parsed = EndpointParser(my_api19, 'get')\n        assert parsed.status_code == 200\n        assert parsed.response_data == ['1', '2']\n\n    async def test_my_api20(self):\n        parsed = EndpointParser(my_api20, 'get')\n        assert parsed.status_code == 200\n        assert parsed.response_data == 'Hello World!'\n\n    async def test_my_api21(self):\n        parsed = EndpointParser(my_api21, 'get')\n        assert parsed.status_code == 200\n        assert parsed.response_data == True\n\n    async def test_my_api22(self):\n        parsed = EndpointParser(my_api22, 'get')\n        assert parsed.status_code == 200\n        assert parsed.response_data == [6, 9]\n\n    async def test_my_api23(self):\n        parsed = EndpointParser(my_api23, 'get')\n        assert parsed.status_code == 207\n        assert parsed.response_data == {'detail': 'Hello'}\n\n    async def test_my_api24(self):\n        parsed = EndpointParser(my_api24, 'get')\n        assert parsed.status_code == 200\n        assert parsed.response_data == {'name': 'Ali', 'children': ['A', 'B', 'C']}\n\n    async def test_my_api25(self):\n        parsed = EndpointParser(my_api25, 'get')\n        assert parsed.status_code == 200\n        assert parsed.response_data == {'title': 'Book'}\n\n    async def test_my_api26(self):\n        parsed = EndpointParser(my_api26, 'get')\n        assert parsed.status_code == 200\n        assert parsed.response_data == {'name': 'Ali'}\n\n    async def test_my_api27(self):\n        parsed = EndpointParser(my_api27, 'get')\n        assert parsed.status_code == 200\n        assert parsed.response_data == {'title': 'Book'}\n"
  },
  {
    "path": "tests/test_panel.py",
    "content": "from pathlib import Path\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom panther import Panther\nfrom panther.configs import config\nfrom panther.db import Model\nfrom panther.db.connections import db\nfrom panther.db.models import BaseUser\nfrom panther.panel.urls import url_routing\nfrom panther.test import APIClient\n\n\nclass CustomUser(BaseUser):\n    username: str\n    password: str\n\n\nclass CustomModel(Model):\n    name: str\n    description: str = 'Default description'\n    is_active: bool = True\n\n\n# Test configuration\nAUTHENTICATION = 'panther.authentications.JWTAuthentication'\nSECRET_KEY = 'hvdhRspoTPh1cJVBHcuingQeOKNc1uRhIP2k7suLe2g='\nDB_PATH = 'test_panel.pdb'\nDATABASE = {\n    'engine': {\n        'class': 'panther.db.connections.PantherDBConnection',\n        'path': DB_PATH,\n    },\n}\nUSER_MODEL = 'tests.test_panel.CustomUser'\n\n\nclass TestPanel(IsolatedAsyncioTestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        app = Panther(__name__, configs=__name__, urls={'': url_routing})\n        cls.client = APIClient(app=app)\n\n    def tearDown(self) -> None:\n        db.session.collection('CustomUser').drop()\n        db.session.collection('CustomModel').drop()\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n        Path(DB_PATH).unlink(missing_ok=True)\n\n    @classmethod\n    def check_in_cookies(cls, *args, cookies):\n        cookies = {cookie[1] for cookie in cookies}\n        for arg in args:\n            for c in cookies:\n                c.startswith(arg)\n                break\n            else:\n                assert False\n\n    async def test_login_view_get(self):\n        \"\"\"Test GET request to login page\"\"\"\n        res = await self.client.get('login/')\n        assert res.status_code == 200\n        assert res.headers['Content-Type'] == 'text/html; charset=utf-8'\n\n    async def test_login_view_post_success(self):\n        \"\"\"Test successful login POST request\"\"\"\n        # Create a test user\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n\n        # Test successful login\n        res = await self.client.post(\n            path='login/',\n            payload={'username': 'testuser', 'password': 'testpass'},\n        )\n        self.check_in_cookies(b'access_token', b'refresh_token', cookies=res.cookies)\n        assert res.status_code == 302  # Redirect\n\n    async def test_login_view_post_user_not_found(self):\n        \"\"\"Test login POST request with non-existent user\"\"\"\n        res = await self.client.post(\n            path='login/',\n            payload={'username': 'nonexistent', 'password': 'testpass'},\n        )\n\n        assert res.status_code == 400\n        assert res.headers['Content-Type'] == 'text/html; charset=utf-8'\n\n    async def test_login_view_post_wrong_password(self):\n        \"\"\"Test login POST request with wrong password\"\"\"\n        # Create a test user\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n\n        # Test wrong password\n        res = await self.client.post(\n            path='login/',\n            payload={'username': 'testuser', 'password': 'wrongpass'},\n        )\n\n        assert res.status_code == 400\n        assert res.headers['Content-Type'] == 'text/html; charset=utf-8'\n\n    async def test_login_view_post_with_redirect(self):\n        \"\"\"Test login POST request with redirect_to parameter\"\"\"\n        # Create a test user\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n\n        # Test login with redirect\n        res = await self.client.post(\n            path='login/',\n            payload={'username': 'testuser', 'password': 'testpass'},\n            query_params={'redirect_to': '/admin/dashboard'},\n        )\n\n        assert res.status_code == 302\n        self.check_in_cookies(b'access_token', b'refresh_token', cookies=res.cookies)\n\n    async def test_home_view_without_auth(self):\n        \"\"\"Test home view without authentication\"\"\"\n        res = await self.client.get('')\n        assert res.status_code == 302  # Should redirect to login\n\n    async def test_home_view_with_auth(self):\n        \"\"\"Test home view with authentication\"\"\"\n        # Create and login user\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n        tokens = await user.login()\n\n        # Test home view with auth\n        res = await self.client.get(path='', headers={'Cookie': f'access_token={tokens[\"access_token\"]}'})\n\n        assert res.status_code == 200\n        assert res.headers['Content-Type'] == 'text/html; charset=utf-8'\n\n    async def test_table_view_without_auth(self):\n        \"\"\"Test table view without authentication\"\"\"\n        res = await self.client.get('0/')\n        assert res.status_code == 302  # Should redirect to login\n\n    async def test_table_view_with_auth_empty_data(self):\n        \"\"\"Test table view with authentication but no data\"\"\"\n        # Create and login user\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n        tokens = await user.login()\n\n        # Test table view with auth but no data\n        res = await self.client.get(path='0/', headers={'Cookie': f'access_token={tokens[\"access_token\"]}'})\n\n        assert res.status_code == 200\n        assert res.headers['Content-Type'] == 'text/html; charset=utf-8'\n\n    async def test_table_view_with_auth_with_data(self):\n        \"\"\"Test table view with authentication and data\"\"\"\n        # Create and login user\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n        tokens = await user.login()\n\n        # Create some test data\n        await CustomModel.insert_one(name='Test Item 1', description='Description 1')\n        await CustomModel.insert_one(name='Test Item 2', description='Description 2')\n\n        # Test table view with auth and data\n        res = await self.client.get(\n            path='1/',  # CustomModel should be at index 1 (after CustomUser at index 0)\n            headers={'Cookie': f'access_token={tokens[\"access_token\"]}'},\n        )\n\n        assert res.status_code == 200\n        assert res.headers['Content-Type'] == 'text/html; charset=utf-8'\n\n    async def test_create_view_get_without_auth(self):\n        \"\"\"Test create view GET without authentication\"\"\"\n        res = await self.client.get('0/create/')\n        assert res.status_code == 302  # Should redirect to login\n\n    async def test_create_view_get_with_auth(self):\n        \"\"\"Test create view GET with authentication\"\"\"\n        # Create and login user\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n        tokens = await user.login()\n\n        # Test create view GET with auth\n        res = await self.client.get(path='0/create/', headers={'Cookie': f'access_token={tokens[\"access_token\"]}'})\n\n        assert res.status_code == 200\n        assert res.headers['Content-Type'] == 'text/html; charset=utf-8'\n\n    async def test_create_view_post_without_auth(self):\n        \"\"\"Test create view POST without authentication\"\"\"\n        res = await self.client.post('0/create/', payload={'username': 'newuser', 'password': 'newpass'})\n        assert res.status_code == 302  # Should redirect to login\n\n    async def test_create_view_post_with_auth_user_model(self):\n        \"\"\"Test create view POST with authentication for user model\"\"\"\n        # Create and login user\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n        tokens = await user.login()\n\n        # Test create user with auth\n        res = await self.client.post(\n            path='0/create/',\n            payload={'username': 'newuser', 'password': 'newpass'},\n            headers={'Cookie': f'access_token={tokens[\"access_token\"]}'},\n        )\n\n        assert res.status_code == 200\n        assert res.data['username'] == 'newuser'\n        # Password should be hashed\n        assert res.data['password'] != 'newpass'\n\n    async def test_create_view_post_with_auth_regular_model(self):\n        \"\"\"Test create view POST with authentication for regular model\"\"\"\n        # Create and login user\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n        tokens = await user.login()\n\n        # Test create regular model with auth\n        res = await self.client.post(\n            path='1/create/',\n            payload={'name': 'New Item', 'description': 'New Description', 'is_active': True},\n            headers={'Cookie': f'access_token={tokens[\"access_token\"]}'},\n        )\n\n        assert res.status_code == 200\n        assert res.data['name'] == 'New Item'\n        assert res.data['description'] == 'New Description'\n        assert res.data['is_active'] is True\n\n    async def test_detail_view_get_without_auth(self):\n        \"\"\"Test detail view GET without authentication\"\"\"\n        res = await self.client.get('0/test-id/')\n        assert res.status_code == 302  # Should redirect to login\n\n    async def test_detail_view_get_with_auth(self):\n        \"\"\"Test detail view GET with authentication\"\"\"\n        # Create and login user\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n        tokens = await user.login()\n\n        # Create a test item\n        test_item = await CustomModel.insert_one(name='Test Item', description='Test Description')\n\n        # Test detail view GET with auth\n        res = await self.client.get(\n            path=f'1/{test_item.id}/', headers={'Cookie': f'access_token={tokens[\"access_token\"]}'}\n        )\n\n        assert res.status_code == 200\n        assert res.headers['Content-Type'] == 'text/html; charset=utf-8'\n\n    async def test_detail_view_put_without_auth(self):\n        \"\"\"Test detail view PUT without authentication\"\"\"\n        res = await self.client.put('0/test-id/', payload={'username': 'updated'})\n        assert res.status_code == 302  # Should redirect to login\n\n    async def test_detail_view_put_with_auth(self):\n        \"\"\"Test detail view PUT with authentication\"\"\"\n        # Create and login user\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n        tokens = await user.login()\n\n        # Create a test item\n        test_item = await CustomModel.insert_one(name='Test Item', description='Test Description')\n\n        # Test detail view PUT with auth\n        res = await self.client.put(\n            path=f'1/{test_item.id}/',\n            payload={'name': 'Updated Item', 'description': 'Updated Description'},\n            headers={'Cookie': f'access_token={tokens[\"access_token\"]}'},\n        )\n\n        assert res.status_code == 200\n        assert res.data['name'] == 'Updated Item'\n        assert res.data['description'] == 'Updated Description'\n\n    async def test_detail_view_delete_without_auth(self):\n        \"\"\"Test detail view DELETE without authentication\"\"\"\n        res = await self.client.delete('0/test-id/')\n        assert res.status_code == 302  # Should redirect to login\n\n    async def test_detail_view_delete_with_auth(self):\n        \"\"\"Test detail view DELETE with authentication\"\"\"\n        # Create and login user\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n        tokens = await user.login()\n\n        # Create a test item\n        test_item = await CustomModel.insert_one(name='Test Item', description='Test Description')\n\n        # Test detail view DELETE with auth\n        res = await self.client.delete(\n            path=f'1/{test_item.id}/', headers={'Cookie': f'access_token={tokens[\"access_token\"]}'}\n        )\n\n        assert res.status_code == 204  # No content\n\n        # Verify item was deleted\n        deleted_item = await CustomModel.find_one(id=test_item.id)\n        assert deleted_item is None\n\n    async def test_invalid_model_index(self):\n        \"\"\"Test accessing invalid model index\"\"\"\n        # Create and login user\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n        tokens = await user.login()\n\n        # Test invalid model index\n        res = await self.client.get(path='999/', headers={'Cookie': f'access_token={tokens[\"access_token\"]}'})\n\n        assert res.status_code == 500  # Should raise an error for invalid index\n\n    async def test_invalid_document_id(self):\n        \"\"\"Test accessing invalid document ID\"\"\"\n        # Create and login user\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n        tokens = await user.login()\n\n        # Test invalid document ID\n        res = await self.client.get(path='0/invalid-id/', headers={'Cookie': f'access_token={tokens[\"access_token\"]}'})\n\n        assert res.status_code == 404  # Should raise an error for invalid document ID\n\n    async def test_middleware_redirect_to_slash(self):\n        \"\"\"Test RedirectToSlashMiddleware functionality\"\"\"\n        # Create and login user\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n        tokens = await user.login()\n\n        # Test without trailing slash (should redirect)\n        res = await self.client.get(path='login', headers={'Cookie': f'access_token={tokens[\"access_token\"]}'})\n        assert res.status_code == 307  # Redirect to add trailing slash\n\n    async def test_cookie_authentication_invalid_token(self):\n        \"\"\"Test cookie authentication with invalid token\"\"\"\n        res = await self.client.get(path='', headers={'Cookie': 'access_token=invalid_token'})\n\n        assert res.status_code == 302  # Should redirect to login\n\n    async def test_cookie_authentication_expired_token(self):\n        \"\"\"Test cookie authentication with expired token\"\"\"\n        # Create a user but don't use their token\n        user = await CustomUser.insert_one(username='testuser', password='testpass')\n        await user.set_password('testpass')\n\n        # Use an expired token\n        expired_token = (\n            'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJpYXQiOjE2MzQ1Njc4NzQsImV4cCI6MTYzNDU2Nzg3NX0.invalid_signature'\n        )\n\n        res = await self.client.get(path='', headers={'Cookie': f'access_token={expired_token}'})\n\n        assert res.status_code == 302  # Should redirect to login\n"
  },
  {
    "path": "tests/test_permission.py",
    "content": "from unittest import IsolatedAsyncioTestCase\n\nfrom panther import Panther\nfrom panther.app import API, GenericAPI\nfrom panther.configs import config\nfrom panther.exceptions import PantherError\nfrom panther.permissions import BasePermission\nfrom panther.request import Request\nfrom panther.test import APIClient\n\n\nclass AlwaysDeniedPermission(BasePermission):\n    async def __call__(self, request: Request) -> bool:\n        return False\n\n\nclass NotInheritedPermission:\n    async def __call__(self, request: Request) -> bool:\n        return False\n\n\nclass SyncPermission(BasePermission):\n    def __call__(self, request: Request) -> bool:\n        return False\n\n\nclass WithoutParamPermission:\n    def __call__(self) -> bool:\n        return False\n\n\ndef without_param_permission():\n    return False\n\n\nclass WithoutCallPermission:\n    pass\n\n\ndef sync_permission(req):\n    return False\n\n\nasync def accept_permission(req):\n    return True\n\n\nasync def deny_permission(req):\n    return True\n\n\n@API()\nasync def without_permission_api(request: Request):\n    return request.user\n\n\n@API(permissions=[AlwaysDeniedPermission])\nasync def denied_permission_api(request: Request):\n    return request.user\n\n\n@API(permissions=AlwaysDeniedPermission)\nasync def single_denied_permission_api(request: Request):\n    return request.user\n\n\n@API(permissions=[NotInheritedPermission])\nasync def not_inherited_permission_api(request: Request):\n    return request.user\n\n\n@API(permissions=[accept_permission, accept_permission, AlwaysDeniedPermission])\nasync def multiple_permissions_api(request: Request):\n    return request.user\n\n\nurls = {\n    'without': without_permission_api,\n    'denied-permission': denied_permission_api,\n    'single-denied-permission': single_denied_permission_api,\n    'not-inherited-permission': not_inherited_permission_api,\n    'multiple-permission': multiple_permissions_api,\n}\n\n\nclass TestPermission(IsolatedAsyncioTestCase):\n    def setUp(self) -> None:\n        app = Panther(__name__, configs=__name__, urls=urls)\n        self.client = APIClient(app=app)\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n\n    async def test_without_permission(self):\n        res = await self.client.get('without')\n        assert res.status_code == 200\n        assert res.data is None\n\n    async def test_denied_permission(self):\n        with self.assertNoLogs(level='ERROR'):\n            res = await self.client.get('denied-permission')\n        assert res.status_code == 403\n        assert res.data['detail'] == 'Permission Denied'\n\n    async def test_single_denied_permission(self):\n        with self.assertNoLogs(level='ERROR'):\n            res = await self.client.get('single-denied-permission')\n        assert res.status_code == 403\n        assert res.data['detail'] == 'Permission Denied'\n\n    async def test_not_inherited_permission(self):\n        with self.assertNoLogs(level='ERROR'):\n            res = await self.client.get('not-inherited-permission')\n        assert res.status_code == 403\n        assert res.data['detail'] == 'Permission Denied'\n\n    async def test_multiple_permission(self):\n        with self.assertNoLogs(level='ERROR'):\n            res = await self.client.get('multiple-permission')\n        assert res.status_code == 403\n        assert res.data['detail'] == 'Permission Denied'\n\n    async def test_sync_permission(self):\n        try:\n\n            @API(permissions=[SyncPermission])\n            async def sync_permission_api(request: Request):\n                return request.user\n        except PantherError as e:\n            assert e.args[0] == 'SyncPermission.__call__() should be `async`'\n        else:\n            assert False\n\n    async def test_sync_permission_classbased(self):\n        try:\n\n            class SyncPermissionAPI(GenericAPI):\n                permissions = [SyncPermission]\n        except PantherError as e:\n            assert e.args[0] == 'SyncPermission.__call__() should be `async`'\n        else:\n            assert False\n\n    async def test_without_call_permission(self):\n        try:\n\n            @API(permissions=[WithoutCallPermission])\n            async def permission_api(request: Request):\n                return request.user\n        except PantherError as e:\n            assert e.args[0] == 'WithoutCallPermission must implement __call__() method.'\n        else:\n            assert False\n\n    async def test_without_call_permission_classbased(self):\n        try:\n\n            class PermissionAPI(GenericAPI):\n                permissions = [WithoutCallPermission]\n        except PantherError as e:\n            assert e.args[0] == 'WithoutCallPermission must implement __call__() method.'\n        else:\n            assert False\n\n    async def test_sync_function_permission(self):\n        try:\n\n            @API(permissions=[sync_permission])\n            async def permission_api(request: Request):\n                return request.user\n        except PantherError as e:\n            assert e.args[0] == 'sync_permission() should be `async`'\n        else:\n            assert False\n\n    async def test_sync_function_permission_classbased(self):\n        try:\n\n            class PermissionAPI(GenericAPI):\n                permissions = [sync_permission]\n        except PantherError as e:\n            assert e.args[0] == 'sync_permission() should be `async`'\n        else:\n            assert False\n\n    async def test_class_without_param_permission(self):\n        try:\n\n            @API(permissions=[WithoutParamPermission])\n            async def permission_api(request: Request):\n                return request.user\n        except PantherError as e:\n            assert e.args[0] == 'WithoutParamPermission.__call__() requires 2 positional argument(s) (self, request).'\n        else:\n            assert False\n\n    async def test_class_without_param_permission_classbased(self):\n        try:\n\n            class PermissionAPI(GenericAPI):\n                permissions = [WithoutParamPermission]\n        except PantherError as e:\n            assert e.args[0] == 'WithoutParamPermission.__call__() requires 2 positional argument(s) (self, request).'\n        else:\n            assert False\n\n    async def test_function_without_param_permission(self):\n        try:\n\n            @API(permissions=[without_param_permission])\n            async def permission_api(request: Request):\n                return request.user\n        except PantherError as e:\n            assert e.args[0] == 'without_param_permission() requires 1 positional argument(s) (request).'\n        else:\n            assert False\n\n    async def test_function_without_param_permission_classbased(self):\n        try:\n\n            class PermissionAPI(GenericAPI):\n                permissions = [without_param_permission]\n        except PantherError as e:\n            assert e.args[0] == 'without_param_permission() requires 1 positional argument(s) (request).'\n        else:\n            assert False\n"
  },
  {
    "path": "tests/test_request.py",
    "content": "from unittest import IsolatedAsyncioTestCase\n\nimport orjson as json\n\nfrom panther import Panther\nfrom panther.app import API, GenericAPI\nfrom panther.configs import config\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.test import APIClient\n\n\n@API()\nasync def request_path(request: Request):\n    return request.path\n\n\n@API()\nasync def request_client(request: Request):\n    return request.client.__dict__\n\n\n@API()\nasync def request_query_params(request: Request):\n    return request.query_params\n\n\n@API()\nasync def request_data(request: Request):\n    return request.data\n\n\n@API()\nasync def request_path_variables(name: str, age: int, is_alive: bool):\n    return {'name': name, 'age': age, 'is_alive': is_alive}\n\n\n@API()\nasync def request_header(request: Request):\n    return request.headers.__dict__\n\n\n@API()\nasync def request_header_str(request: Request):\n    return request.headers.__str__(), request.headers.__repr__()\n\n\n@API()\nasync def request_header_contains(request: Request, header: str):\n    return header in request.headers\n\n\n@API()\nasync def request_header_by_attr(request: Request):\n    return request.headers.authorization\n\n\n@API()\nasync def request_header_by_item(request: Request):\n    return request.headers['Authorization'], request.headers['authorization']\n\n\n# # # Methods\nclass AllMethods(GenericAPI):\n    def get(self, *args, **kwargs):\n        return Response()\n\n    def post(self, *args, **kwargs):\n        return Response()\n\n    def put(self, *args, **kwargs):\n        return Response()\n\n    def patch(self, *args, **kwargs):\n        return Response()\n\n    def delete(self, *args, **kwargs):\n        return Response()\n\n\n@API()\nasync def all_methods():\n    return Response()\n\n\nclass GetMethod(GenericAPI):\n    def get(self, *args, **kwargs):\n        return Response()\n\n\n@API(methods=['GET'])\nasync def get_method():\n    return Response()\n\n\nclass PostMethod(GenericAPI):\n    def post(self, *args, **kwargs):\n        return Response()\n\n\n@API(methods=['POST'])\nasync def post_method():\n    return Response()\n\n\nclass PutMethod(GenericAPI):\n    def put(self, *args, **kwargs):\n        return Response()\n\n\n@API(methods=['PUT'])\nasync def put_method():\n    return Response()\n\n\nclass PatchMethod(GenericAPI):\n    def patch(self, *args, **kwargs):\n        return Response()\n\n\n@API(methods=['PATCH'])\nasync def patch_method():\n    return Response()\n\n\nclass DeleteMethod(GenericAPI):\n    def delete(self, *args, **kwargs):\n        return Response()\n\n\n@API(methods=['DELETE'])\nasync def delete_method():\n    return Response()\n\n\nclass GetPostPatchMethods(GenericAPI):\n    def get(self, *args, **kwargs):\n        return Response()\n\n    def post(self, *args, **kwargs):\n        return Response()\n\n    def patch(self, *args, **kwargs):\n        return Response()\n\n\n@API(methods=['GET', 'POST', 'PATCH'])\nasync def get_post_patch_methods():\n    return Response()\n\n\nurls = {\n    'path': request_path,\n    'client': request_client,\n    'query-params': request_query_params,\n    'data': request_data,\n    'path/<name>/variable/<age>/<is_alive>/': request_path_variables,\n    'header': request_header,\n    'header-str': request_header_str,\n    'header-attr': request_header_by_attr,\n    'header-item': request_header_by_item,\n    'header-contains/<header>/': request_header_contains,\n    'all-func': all_methods,\n    'all-class': AllMethods,\n    'get-func': get_method,\n    'get-class': GetMethod,\n    'post-func': post_method,\n    'post-class': PostMethod,\n    'put-func': put_method,\n    'put-class': PutMethod,\n    'patch-func': patch_method,\n    'patch-class': PatchMethod,\n    'delete-func': delete_method,\n    'delete-class': DeleteMethod,\n    'get-post-patch-func': get_post_patch_methods,\n    'get-post-patch-class': GetPostPatchMethods,\n}\n\n\nclass TestRequest(IsolatedAsyncioTestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        app = Panther(__name__, configs=__name__, urls=urls)\n        cls.client = APIClient(app=app)\n\n    @classmethod\n    def tearDownClass(cls) -> None:\n        config.refresh()\n\n    async def test_path(self):\n        res = await self.client.get('path/')\n        assert res.status_code == 200\n        assert res.data == '/path/'\n\n    async def test_client(self):\n        res = await self.client.get('client/')\n        assert res.status_code == 200\n        assert res.data == {'ip': '127.0.0.1', 'port': 8585}\n\n    async def test_query_params(self):\n        res = await self.client.get(\n            'query-params/',\n            query_params={'my': 'name', 'is': 'ali', 'how': 'are'},\n        )\n        assert res.status_code == 200\n        assert res.data == {'my': 'name', 'is': 'ali', 'how': 'are'}\n\n    async def test_data(self):\n        payload = {'detail': 'ok'}\n        res = await self.client.post('data/', payload=json.dumps(payload))\n        assert res.status_code == 200\n        assert res.data == payload\n\n    async def test_path_variables(self):\n        res = await self.client.post('path/Ali/variable/27/true/')\n        expected_response = {'name': 'Ali', 'age': 27, 'is_alive': True}\n        assert res.status_code == 200\n        assert res.data == expected_response\n\n    # # # Headers\n    async def test_headers_none(self):\n        res = await self.client.get('header')\n        expected_headers = {}\n        assert res.data == expected_headers\n\n    async def test_headers_content_type(self):\n        res = await self.client.post('header')\n        expected_headers = {'content-type': 'application/json'}\n        assert res.data == expected_headers\n\n    async def test_headers_full_items(self):\n        headers = {\n            'User-Agent': 'PostmanRuntime/7.36.0',\n            'Accept': '*/*',\n            'Cache-Control': 'no-cache',\n            'Host': '127.0.0.1:8000',\n            'Accept-Encoding': 'gzip, deflate, br',\n            'Connection': 'keep-alive',\n            'Content-Length': 546,\n        }\n        res = await self.client.post('header', headers=headers)\n        expected_headers = {\n            'content-type': 'application/json',\n            'User-Agent': 'PostmanRuntime/7.36.0',\n            'Accept': '*/*',\n            'Cache-Control': 'no-cache',\n            'Host': '127.0.0.1:8000',\n            'Accept-Encoding': 'gzip, deflate, br',\n            'Connection': 'keep-alive',\n            'Content-Length': '546',\n        }\n        assert res.data == expected_headers\n\n    async def test_headers_unknown_items(self):\n        headers = {\n            'Header1': 'PostmanRuntime/7.36.0',\n            'Header2': '*/*',\n        }\n        res = await self.client.post('header', headers=headers)\n        expected_headers = {\n            'content-type': 'application/json',\n            'Header1': 'PostmanRuntime/7.36.0',\n            'Header2': '*/*',\n        }\n        assert res.data == expected_headers\n\n    async def test_headers_authorization_by_getattr(self):\n        headers = {\n            'Authorization': 'Token xxx',\n        }\n        res = await self.client.post('header-attr', headers=headers)\n        assert res.data == 'Token xxx'\n\n    async def test_headers_authorization_by_getitem(self):\n        headers = {\n            'Authorization': 'Token xxx',\n        }\n        res = await self.client.post('header-item', headers=headers)\n        assert res.data == ['Token xxx', 'Token xxx']\n\n    async def test_headers_str(self):\n        res = await self.client.post('header-str')\n        assert res.data == ['Headers(content-type=application/json)', 'Headers(content-type=application/json)']\n\n    async def test_headers_contains(self):\n        res = await self.client.post('header-contains/content-type/')\n        assert res.data is True\n        res = await self.client.post('header-contains/accept/')\n        assert res.data is False\n\n    # # # Methods\n    async def test_method_all(self):\n        res_func = await self.client.get('all-func/')\n        res_class = await self.client.get('all-class/')\n        assert res_func.status_code == 200\n        assert res_class.status_code == 200\n\n        res_func = await self.client.post('all-func/')\n        res_class = await self.client.post('all-class/')\n        assert res_func.status_code == 200\n        assert res_class.status_code == 200\n\n        res_func = await self.client.put('all-func/')\n        res_class = await self.client.put('all-class/')\n        assert res_func.status_code == 200\n        assert res_class.status_code == 200\n\n        res_func = await self.client.patch('all-func/')\n        res_class = await self.client.patch('all-class/')\n        assert res_func.status_code == 200\n        assert res_class.status_code == 200\n\n        res_func = await self.client.delete('all-func/')\n        res_class = await self.client.delete('all-class/')\n        assert res_func.status_code == 200\n        assert res_class.status_code == 200\n\n    async def test_method_get(self):\n        res_func = await self.client.get('get-func/')\n        res_class = await self.client.get('get-class/')\n        assert res_func.status_code == 200\n        assert res_class.status_code == 200\n\n        res_func = await self.client.post('get-func/')\n        res_class = await self.client.post('get-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.put('get-func/')\n        res_class = await self.client.put('get-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.patch('get-func/')\n        res_class = await self.client.patch('get-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.delete('get-func/')\n        res_class = await self.client.delete('get-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n    async def test_method_post(self):\n        res_func = await self.client.get('post-func/')\n        res_class = await self.client.get('post-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.post('post-func/')\n        res_class = await self.client.post('post-class/')\n        assert res_func.status_code == 200\n        assert res_class.status_code == 200\n\n        res_func = await self.client.put('post-func/')\n        res_class = await self.client.put('post-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.patch('post-func/')\n        res_class = await self.client.patch('post-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.delete('post-func/')\n        res_class = await self.client.delete('post-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n    async def test_method_put(self):\n        res_func = await self.client.get('put-func/')\n        res_class = await self.client.get('put-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.post('put-func/')\n        res_class = await self.client.post('put-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.put('put-func/')\n        res_class = await self.client.put('put-class/')\n        assert res_func.status_code == 200\n        assert res_class.status_code == 200\n\n        res_func = await self.client.patch('put-func/')\n        res_class = await self.client.patch('put-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.delete('put-func/')\n        res_class = await self.client.delete('put-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n    async def test_method_patch(self):\n        res_func = await self.client.get('patch-func/')\n        res_class = await self.client.get('patch-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.post('patch-func/')\n        res_class = await self.client.post('patch-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.put('patch-func/')\n        res_class = await self.client.put('patch-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.patch('patch-func/')\n        res_class = await self.client.patch('patch-class/')\n        assert res_func.status_code == 200\n        assert res_class.status_code == 200\n\n        res_func = await self.client.delete('patch-func/')\n        res_class = await self.client.delete('patch-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n    async def test_method_delete(self):\n        res_func = await self.client.get('delete-func/')\n        res_class = await self.client.get('delete-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.post('delete-func/')\n        res_class = await self.client.post('delete-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.put('delete-func/')\n        res_class = await self.client.put('delete-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.patch('delete-func/')\n        res_class = await self.client.patch('delete-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.delete('delete-func/')\n        res_class = await self.client.delete('delete-class/')\n        assert res_func.status_code == 200\n        assert res_class.status_code == 200\n\n    async def test_method_get_post_patch(self):\n        res_func = await self.client.get('get-post-patch-func/')\n        res_class = await self.client.get('get-post-patch-class/')\n        assert res_func.status_code == 200\n        assert res_class.status_code == 200\n\n        res_func = await self.client.post('get-post-patch-func/')\n        res_class = await self.client.post('get-post-patch-class/')\n        assert res_func.status_code == 200\n        assert res_class.status_code == 200\n\n        res_func = await self.client.put('get-post-patch-func/')\n        res_class = await self.client.put('get-post-patch-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n\n        res_func = await self.client.patch('get-post-patch-func/')\n        res_class = await self.client.patch('get-post-patch-class/')\n        assert res_func.status_code == 200\n        assert res_class.status_code == 200\n\n        res_func = await self.client.delete('get-post-patch-func/')\n        res_class = await self.client.delete('get-post-patch-class/')\n        assert res_func.status_code == 405\n        assert res_class.status_code == 405\n"
  },
  {
    "path": "tests/test_request_isolation.py",
    "content": "import asyncio\nfrom typing import ClassVar\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom panther import Panther\nfrom panther.app import API, GenericAPI\nfrom panther.configs import config\nfrom panther.request import Request\nfrom panther.test import APIClient\n\n\nclass RequestIsolationRaceAuth:\n    seen_paths: ClassVar[set[str]] = set()\n    second_request_seen: ClassVar[asyncio.Event | None] = None\n\n    @classmethod\n    def reset(cls) -> None:\n        cls.seen_paths.clear()\n        cls.second_request_seen = asyncio.Event()\n\n    async def __call__(self, request: Request) -> str:\n        cls = type(self)\n        if cls.second_request_seen is None:\n            cls.second_request_seen = asyncio.Event()\n\n        cls.seen_paths.add(request.path)\n        if request.path.endswith('/second/'):\n            cls.second_request_seen.set()\n        if request.path.endswith('/first/'):\n            await cls.second_request_seen.wait()\n            await asyncio.sleep(0)\n        return 'user'\n\n\n@API(auth=RequestIsolationRaceAuth)\nasync def function_api_race_endpoint(name: str, request: Request):\n    return {'name': name, 'path': request.path}\n\n\nclass ClassBasedRaceEndpoint(GenericAPI):\n    auth = RequestIsolationRaceAuth\n\n    async def get(self, name: str, request: Request):\n        return {'name': name, 'path': request.path}\n\n\ntest_urls = {\n    'race/<name>/': function_api_race_endpoint,\n    'race-class/<name>/': ClassBasedRaceEndpoint,\n}\n\n\nclass TestAPIRequestIsolation(IsolatedAsyncioTestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        app = Panther(__name__, configs=__name__, urls=test_urls)\n        cls.client = APIClient(app=app)\n\n    @classmethod\n    def tearDownClass(cls) -> None:\n        config.refresh()\n\n    async def test_function_api_keeps_request_isolation_under_concurrency(self):\n        RequestIsolationRaceAuth.reset()\n        first_response, second_response = await asyncio.gather(\n            self.client.get('race/first/'),\n            self.client.get('race/second/'),\n        )\n\n        assert first_response.status_code == 200\n        assert second_response.status_code == 200\n        assert first_response.data == {'name': 'first', 'path': '/race/first/'}\n        assert second_response.data == {'name': 'second', 'path': '/race/second/'}\n\n    async def test_class_based_apis_keep_request_isolation(self):\n        RequestIsolationRaceAuth.reset()\n        first_response, second_response = await asyncio.gather(\n            self.client.get('race-class/first/'),\n            self.client.get('race-class/second/'),\n        )\n\n        assert first_response.status_code == 200\n        assert second_response.status_code == 200\n        assert first_response.data == {'name': 'first', 'path': '/race-class/first/'}\n        assert second_response.data == {'name': 'second', 'path': '/race-class/second/'}\n"
  },
  {
    "path": "tests/test_response.py",
    "content": "import platform\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom pydantic import BaseModel\n\nfrom panther import Panther\nfrom panther.app import API, GenericAPI\nfrom panther.configs import config\nfrom panther.db import Model\nfrom panther.response import (\n    Cookie,\n    FileResponse,\n    HTMLResponse,\n    PlainTextResponse,\n    Response,\n    StreamingResponse,\n    TemplateResponse,\n)\nfrom panther.test import APIClient\n\n\n@API()\nasync def return_nothing():\n    pass\n\n\nclass ReturnNothing(GenericAPI):\n    def get(self):\n        pass\n\n\n@API()\nasync def return_none():\n    return None\n\n\nclass ReturnNone(GenericAPI):\n    def get(self):\n        return None\n\n\n@API()\nasync def return_string():\n    return 'Hello'\n\n\nclass ReturnString(GenericAPI):\n    def get(self):\n        return 'Hello'\n\n\n@API()\nasync def return_dict():\n    return {'detail': 'ok'}\n\n\nclass ReturnDict(GenericAPI):\n    def get(self):\n        return {'detail': 'ok'}\n\n\n@API()\nasync def return_list():\n    return [1, 2, 3]\n\n\nclass ReturnList(GenericAPI):\n    def get(self):\n        return [1, 2, 3]\n\n\n@API()\nasync def return_tuple():\n    return 1, 2, 3, 4\n\n\nclass ReturnTuple(GenericAPI):\n    def get(self):\n        return 1, 2, 3, 4\n\n\n@API()\nasync def return_response_none():\n    return Response()\n\n\nclass ReturnResponseNone(GenericAPI):\n    def get(self):\n        return Response()\n\n\n@API()\nasync def return_response_dict():\n    return Response(data={'detail': 'ok'})\n\n\nclass ReturnResponseDict(GenericAPI):\n    def get(self):\n        return Response(data={'detail': 'ok'})\n\n\n@API()\nasync def return_response_list():\n    return Response(data=['car', 'home', 'phone'])\n\n\nclass ReturnResponseList(GenericAPI):\n    def get(self):\n        return Response(data=['car', 'home', 'phone'])\n\n\n@API()\nasync def return_response_tuple():\n    return Response(data=('car', 'home', 'phone', 'book'))\n\n\nclass ReturnResponseTuple(GenericAPI):\n    def get(self):\n        return Response(data=('car', 'home', 'phone', 'book'))\n\n\nclass CustomUser(BaseModel):\n    name: str\n    age: int\n    is_alive: bool\n\n\nclass CustomBook(BaseModel):\n    title: str\n    author: CustomUser\n    readers: list[CustomUser]\n\n\nclass ReturnResponseBaseModel(GenericAPI):\n    def get(self):\n        return Response(data=CustomUser(name='John', age=21, is_alive=True))\n\n\nclass ReturnResponseNestedBaseModel(GenericAPI):\n    def get(self):\n        return Response(\n            data={\n                'name': 'Ali',\n                'book': CustomBook(\n                    title='Boo1',\n                    author=CustomUser(name='John', age=21, is_alive=True),\n                    readers=[\n                        CustomUser(name='Sara', age=22, is_alive=True),\n                        CustomUser(name='Sam', age=5, is_alive=False),\n                    ],\n                ),\n                'user': CustomUser(name='Ali', age=2, is_alive=True),\n                'books': [\n                    CustomBook(title='Book1', author=CustomUser(name='John1', age=21, is_alive=True), readers=[]),\n                    CustomBook(title='Book2', author=CustomUser(name='John2', age=22, is_alive=True), readers=[]),\n                ],\n            }\n        )\n\n\nclass CustomProduct(Model):\n    title: str\n\n\nclass ReturnResponseModel(GenericAPI):\n    def get(self):\n        return Response(data=CustomProduct(title='Fruit'))\n\n\n@API()\nasync def return_html_response():\n    return HTMLResponse('<html><head><title></title></head></html>')\n\n\nclass ReturnHTMLResponse(GenericAPI):\n    def get(self):\n        return HTMLResponse('<html><head><title></title></head></html>')\n\n\n@API()\nasync def return_template_response() -> TemplateResponse:\n    return TemplateResponse(source='<html><body><p>{{ content }}</p></body></html>', context={'content': 'Hello World'})\n\n\nclass ReturnTemplateResponse(GenericAPI):\n    def get(self) -> TemplateResponse:\n        return TemplateResponse(\n            source='<html><body><p>{{ content }}</p></body></html>',\n            context={'content': 'Hello World'},\n        )\n\n\n@API()\nasync def return_file_not_found_response():\n    return FileResponse('random.file')\n\n\n@API()\nasync def return_file_response():\n    return FileResponse('README.md')\n\n\n@API()\nasync def return_plain_response():\n    return PlainTextResponse('Hello World')\n\n\nclass ReturnPlainResponse(GenericAPI):\n    def get(self):\n        return PlainTextResponse('Hello World')\n\n\nclass ReturnStreamingResponse(GenericAPI):\n    def get(self):\n        def f():\n            yield from range(5)\n\n        return StreamingResponse(f())\n\n\nclass ReturnAsyncStreamingResponse(GenericAPI):\n    async def get(self):\n        async def f():\n            for i in range(6):\n                yield i\n\n        return StreamingResponse(f())\n\n\n@API()\ndef full_cookie_api():\n    return Response(\n        set_cookies=Cookie(\n            key='custom_key',\n            value='custom_value',\n            domain='example.com',\n            max_age=100,\n            secure=True,\n            httponly=True,\n            samesite='strict',\n            path='/here/',\n        )\n    )\n\n\n@API()\ndef multiple_cookies_api():\n    return Response(\n        set_cookies=[\n            Cookie(\n                key='custom_key1',\n                value='custom_value1',\n                domain='example.com',\n                max_age=100,\n                secure=True,\n                httponly=True,\n                samesite='strict',\n                path='/here/',\n            ),\n            Cookie(\n                key='custom_key2',\n                value='custom_value2',\n                domain='example.com',\n                max_age=100,\n                secure=True,\n                httponly=True,\n                samesite='strict',\n                path='/here/',\n            ),\n        ]\n    )\n\n\n@API()\ndef default_cookies_api():\n    return Response(set_cookies=Cookie(key='custom_key', value='custom_value'))\n\n\nurls = {\n    'nothing': return_nothing,\n    'none': return_none,\n    'dict': return_dict,\n    'str': return_string,\n    'list': return_list,\n    'tuple': return_tuple,\n    'response-none': return_response_none,\n    'response-dict': return_response_dict,\n    'response-list': return_response_list,\n    'response-tuple': return_response_tuple,\n    'html': return_html_response,\n    'file-not-found': return_file_not_found_response,\n    'file': return_file_response,\n    'plain': return_plain_response,\n    'template': return_template_response,\n    'nothing-cls': ReturnNothing,\n    'none-cls': ReturnNone,\n    'dict-cls': ReturnDict,\n    'str-cls': ReturnString,\n    'list-cls': ReturnList,\n    'tuple-cls': ReturnTuple,\n    'basemodel': ReturnResponseBaseModel,\n    'nested-basemodel': ReturnResponseNestedBaseModel,\n    'model': ReturnResponseModel,\n    'response-none-cls': ReturnResponseNone,\n    'response-dict-cls': ReturnResponseDict,\n    'response-list-cls': ReturnResponseList,\n    'response-tuple-cls': ReturnResponseTuple,\n    'html-cls': ReturnHTMLResponse,\n    'template-cls': ReturnTemplateResponse,\n    'plain-cls': ReturnPlainResponse,\n    'stream': ReturnStreamingResponse,\n    'async-stream': ReturnAsyncStreamingResponse,\n    'full-cookies': full_cookie_api,\n    'multiple-cookies': multiple_cookies_api,\n    'default-cookies': default_cookies_api,\n}\n\n\nclass TestResponses(IsolatedAsyncioTestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        app = Panther(__name__, configs=__name__, urls=urls)\n        cls.client = APIClient(app=app)\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n\n    async def test_nothing(self):\n        res = await self.client.get('nothing/')\n        assert res.status_code == 200\n        assert res.data is None\n        assert res.body == b''\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '0'\n\n    async def test_nothing_cls(self):\n        res = await self.client.get('nothing-cls/')\n        assert res.status_code == 200\n        assert res.data is None\n        assert res.body == b''\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '0'\n\n    async def test_none(self):\n        res = await self.client.get('none/')\n        assert res.status_code == 200\n        assert res.data is None\n        assert res.body == b''\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '0'\n\n    async def test_none_cls(self):\n        res = await self.client.get('none-cls/')\n        assert res.status_code == 200\n        assert res.data is None\n        assert res.body == b''\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '0'\n\n    async def test_dict(self):\n        res = await self.client.get('dict/')\n        assert res.status_code == 200\n        assert res.data == {'detail': 'ok'}\n        assert res.body == b'{\"detail\":\"ok\"}'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '15'\n\n    async def test_dict_cls(self):\n        res = await self.client.get('dict-cls/')\n        assert res.status_code == 200\n        assert res.data == {'detail': 'ok'}\n        assert res.body == b'{\"detail\":\"ok\"}'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '15'\n\n    async def test_string(self):\n        res = await self.client.get('str/')\n        assert res.status_code == 200\n        assert res.data == 'Hello'\n        assert res.body == b'\"Hello\"'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '7'\n\n    async def test_string_cls(self):\n        res = await self.client.get('str-cls/')\n        assert res.status_code == 200\n        assert res.data == 'Hello'\n        assert res.body == b'\"Hello\"'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '7'\n\n    async def test_list(self):\n        res = await self.client.get('list/')\n        assert res.status_code == 200\n        assert res.data == [1, 2, 3]\n        assert res.body == b'[1,2,3]'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '7'\n\n    async def test_list_cls(self):\n        res = await self.client.get('list-cls/')\n        assert res.status_code == 200\n        assert res.data == [1, 2, 3]\n        assert res.body == b'[1,2,3]'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '7'\n\n    async def test_tuple(self):\n        res = await self.client.get('tuple/')\n        assert res.status_code == 200\n        assert res.data == [1, 2, 3, 4]\n        assert res.body == b'[1,2,3,4]'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '9'\n\n    async def test_tuple_cls(self):\n        res = await self.client.get('tuple-cls/')\n        assert res.status_code == 200\n        assert res.data == [1, 2, 3, 4]\n        assert res.body == b'[1,2,3,4]'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '9'\n\n    async def test_basemodel(self):\n        res = await self.client.get('basemodel/')\n        assert res.status_code == 200\n        assert res.data == {'name': 'John', 'age': 21, 'is_alive': True}\n        assert res.body == b'{\"name\":\"John\",\"age\":21,\"is_alive\":true}'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '40'\n\n    async def test_nested_basemodel(self):\n        res = await self.client.get('nested-basemodel/')\n        assert res.status_code == 200\n        assert res.data == {\n            'name': 'Ali',\n            'book': {\n                'title': 'Boo1',\n                'author': {'name': 'John', 'age': 21, 'is_alive': True},\n                'readers': [\n                    {'name': 'Sara', 'age': 22, 'is_alive': True},\n                    {'name': 'Sam', 'age': 5, 'is_alive': False},\n                ],\n            },\n            'user': {'name': 'Ali', 'age': 2, 'is_alive': True},\n            'books': [\n                {'title': 'Book1', 'author': {'name': 'John1', 'age': 21, 'is_alive': True}, 'readers': []},\n                {'title': 'Book2', 'author': {'name': 'John2', 'age': 22, 'is_alive': True}, 'readers': []},\n            ],\n        }\n        assert res.body == (\n            b'{\"name\":\"Ali\",\"book\":{\"title\":\"Boo1\",\"author\":{\"name\":\"John\",\"age\":21,\"is_alive\":true},'\n            b'\"readers\":[{\"name\":\"Sara\",\"age\":22,\"is_alive\":true},{\"name\":\"Sam\",\"age\":5,\"is_alive\":false}]},'\n            b'\"user\":{\"name\":\"Ali\",\"age\":2,\"is_alive\":true},'\n            b'\"books\":[{\"title\":\"Book1\",\"author\":{\"name\":\"John1\",\"age\":21,\"is_alive\":true},\"readers\":[]},'\n            b'{\"title\":\"Book2\",\"author\":{\"name\":\"John2\",\"age\":22,\"is_alive\":true},\"readers\":[]}]}'\n        )\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '401'\n\n    async def test_model(self):\n        res = await self.client.get('model/')\n        assert res.status_code == 200\n        assert res.data == {'id': None, 'title': 'Fruit'}\n        assert res.body == b'{\"id\":null,\"title\":\"Fruit\"}'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '27'\n\n    async def test_response_none(self):\n        res = await self.client.get('response-none/')\n        assert res.status_code == 200\n        assert res.data is None\n        assert res.body == b''\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '0'\n\n    async def test_response_none_cls(self):\n        res = await self.client.get('response-none-cls/')\n        assert res.status_code == 200\n        assert res.data is None\n        assert res.body == b''\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '0'\n\n    async def test_response_dict(self):\n        res = await self.client.get('response-dict/')\n        assert res.status_code == 200\n        assert res.data == {'detail': 'ok'}\n        assert res.body == b'{\"detail\":\"ok\"}'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '15'\n\n    async def test_response_dict_cls(self):\n        res = await self.client.get('response-dict-cls/')\n        assert res.status_code == 200\n        assert res.data == {'detail': 'ok'}\n        assert res.body == b'{\"detail\":\"ok\"}'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '15'\n\n    async def test_response_list(self):\n        res = await self.client.get('response-list/')\n        assert res.status_code == 200\n        assert res.data == ['car', 'home', 'phone']\n        assert res.body == b'[\"car\",\"home\",\"phone\"]'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '22'\n\n    async def test_response_list_cls(self):\n        res = await self.client.get('response-list-cls/')\n        assert res.status_code == 200\n        assert res.data == ['car', 'home', 'phone']\n        assert res.body == b'[\"car\",\"home\",\"phone\"]'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '22'\n\n    async def test_response_tuple(self):\n        res = await self.client.get('response-tuple/')\n        assert res.status_code == 200\n        assert res.data == ['car', 'home', 'phone', 'book']\n        assert res.body == b'[\"car\",\"home\",\"phone\",\"book\"]'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '29'\n\n    async def test_response_tuple_cls(self):\n        res = await self.client.get('response-tuple-cls/')\n        assert res.status_code == 200\n        assert res.data == ['car', 'home', 'phone', 'book']\n        assert res.body == b'[\"car\",\"home\",\"phone\",\"book\"]'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '29'\n\n    async def test_response_html(self):\n        res = await self.client.get('html/')\n        assert res.status_code == 200\n        assert res.data == '<html><head><title></title></head></html>'\n        assert res.body == b'<html><head><title></title></head></html>'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'text/html; charset=utf-8'\n        assert res.headers['Content-Length'] == '41'\n\n    async def test_response_html_cls(self):\n        res = await self.client.get('html-cls/')\n        assert res.status_code == 200\n        assert res.data == '<html><head><title></title></head></html>'\n        assert res.body == b'<html><head><title></title></head></html>'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'text/html; charset=utf-8'\n        assert res.headers['Content-Length'] == '41'\n\n    async def test_response_template(self) -> None:\n        res: Response = await self.client.get('template/')\n        assert res.status_code == 200\n        assert res.data == '<html><body><p>Hello World</p></body></html>'\n        assert res.body == b'<html><body><p>Hello World</p></body></html>'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'text/html; charset=utf-8'\n        assert res.headers['Content-Length'] == '44'\n\n    async def test_response_template_cls(self) -> None:\n        res: Response = await self.client.get('template-cls/')\n        assert res.status_code == 200\n        assert res.data == '<html><body><p>Hello World</p></body></html>'\n        assert res.body == b'<html><body><p>Hello World</p></body></html>'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'text/html; charset=utf-8'\n        assert res.headers['Content-Length'] == '44'\n\n    async def test_response_file_not_found(self):\n        res = await self.client.get('file-not-found/')\n        assert res.status_code == 404\n        assert res.data == {'detail': 'Not Found'}\n        assert res.body == b'{\"detail\":\"Not Found\"}'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'application/json'\n        assert res.headers['Content-Length'] == '22'\n\n    async def test_response_file(self):\n        res = await self.client.get('file/')\n        assert res.status_code == 200\n        assert res.data\n        assert res.body\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'text/markdown'\n        if platform.system() == 'Windows':\n            # Line breaks are \\n\\r\n            assert res.headers['Content-Length'] == '4783'\n        else:\n            assert res.headers['Content-Length'] == '4645'\n\n    async def test_response_plain(self):\n        res = await self.client.get('plain/')\n        assert res.status_code == 200\n        assert res.data == 'Hello World'\n        assert res.body == b'Hello World'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'text/plain; charset=utf-8'\n        assert res.headers['Content-Length'] == '11'\n\n    async def test_response_plain_cls(self):\n        res = await self.client.get('plain-cls/')\n        assert res.status_code == 200\n        assert res.data == 'Hello World'\n        assert res.body == b'Hello World'\n        assert set(res.headers.keys()) == {'Content-Type', 'Content-Length'}\n        assert res.headers['Content-Type'] == 'text/plain; charset=utf-8'\n        assert res.headers['Content-Length'] == '11'\n\n    async def test_streaming_response(self):\n        res = await self.client.get('stream/')\n        assert res.status_code == 200\n        assert res.headers['Content-Type'] == 'application/octet-stream'\n        assert res.data == '01234'\n        assert res.body == b'01234'\n\n    async def test_async_streaming_response(self):\n        res = await self.client.get('async-stream/')\n        assert res.status_code == 200\n        assert res.headers['Content-Type'] == 'application/octet-stream'\n        assert res.data == '012345'\n        assert res.body == b'012345'\n\n    async def test_full_cookie(self):\n        res = await self.client.get('full-cookies/')\n        assert res.status_code == 200\n        assert 'Set-Cookie' in res.headers\n        assert res.headers['Set-Cookie'] == (\n            'custom_key=custom_value; Domain=example.com; HttpOnly; Max-Age=100; Path=/here/; SameSite=strict; Secure'\n        )\n        assert res.cookies == [\n            (\n                b'Set-Cookie',\n                b'custom_key=custom_value; '\n                b'Domain=example.com; '\n                b'HttpOnly; '\n                b'Max-Age=100; '\n                b'Path=/here/; '\n                b'SameSite=strict; '\n                b'Secure',\n            )\n        ]\n\n    async def test_multiple_cookies(self):\n        res = await self.client.get('multiple-cookies/')\n        assert res.status_code == 200\n        assert 'Set-Cookie' in res.headers\n        assert res.cookies == [\n            (\n                b'Set-Cookie',\n                b'custom_key1=custom_value1; Domain=example.com; HttpOnly; Max-Age=100; '\n                b'Path=/here/; SameSite=strict; Secure',\n            ),\n            (\n                b'Set-Cookie',\n                b'custom_key2=custom_value2; Domain=example.com; HttpOnly; Max-Age=100; '\n                b'Path=/here/; SameSite=strict; Secure',\n            ),\n        ]\n\n    async def test_default_cookie(self):\n        res = await self.client.get('default-cookies/')\n        assert res.status_code == 200\n        assert 'Set-Cookie' in res.headers\n        assert res.cookies == [(b'Set-Cookie', b'custom_key=custom_value; Path=/; SameSite=lax')]\n"
  },
  {
    "path": "tests/test_response_output_model.py",
    "content": "from pathlib import Path\nfrom typing import ClassVar\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom pydantic import BaseModel, Field\n\nfrom panther import Panther\nfrom panther.configs import config\nfrom panther.db import Model\nfrom panther.db.connections import db\nfrom panther.db.models import ID\nfrom panther.response import Response\n\n\nclass OutputModelWithAlias(BaseModel):\n    id: ID | None = Field(None, validation_alias='_id', alias='_id')\n    name: str\n\n\nclass OutputModelWithPrepare(BaseModel):\n    value: int\n\n    async def to_response(self, instance, data):\n        # Just return the data with a custom key for test\n        return {'custom': data['value']}\n\n\nclass User(Model):\n    username: str\n    password: str\n\n\nclass UserOutputSerializer(BaseModel):\n    id: str\n    username: str\n\n\nclass OutputModelWithInstanceCheck(BaseModel):\n    value: int\n    called_with: ClassVar = []  # class variable to record calls\n\n    async def to_response(self, instance, data):\n        # Record the arguments for assertion\n        OutputModelWithInstanceCheck.called_with.append((instance, data))\n        return {'value': data['value'], 'instance_type': type(instance).__name__}\n\n\nclass OutputModelWithInstanceCheckIterable(BaseModel):\n    value: int\n    called_with: ClassVar = []\n\n    async def to_response(self, instance, data):\n        OutputModelWithInstanceCheckIterable.called_with.append((instance, data))\n        return {'value': data['value'], 'instance_type': type(instance).__name__}\n\n\nclass TestResponsesOutputModel(IsolatedAsyncioTestCase):\n    DB_PATH = 'test.pdb'\n\n    @classmethod\n    def setUpClass(cls) -> None:\n        global DATABASE\n        DATABASE = {\n            'engine': {'class': 'panther.db.connections.PantherDBConnection', 'path': cls.DB_PATH},\n        }\n        Panther(__name__, configs=__name__, urls={})\n\n    def tearDown(self) -> None:\n        db.session.collection('Book').drop()\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n        Path(cls.DB_PATH).unlink(missing_ok=True)\n\n    async def test_dict(self):\n        resp = Response(data={'_id': 1, 'name': 'foo'})\n        await resp.serialize_output(OutputModelWithAlias)\n        assert resp.data == {'id': '1', 'name': 'foo'}\n\n    async def test_iterable(self):\n        resp = Response(data=[{'_id': 1, 'name': 'foo'}, {'_id': 2, 'name': 'bar'}])\n        await resp.serialize_output(OutputModelWithAlias)\n        assert resp.data == [{'id': '1', 'name': 'foo'}, {'id': '2', 'name': 'bar'}]\n\n    async def test_to_response(self):\n        resp = Response(data={'value': 42})\n        await resp.serialize_output(OutputModelWithPrepare)\n        assert resp.data == {'custom': 42}\n\n    async def test_iterable_to_response(self):\n        resp = Response(data=[{'value': 1}, {'value': 2}])\n        await resp.serialize_output(OutputModelWithPrepare)\n        assert resp.data == [{'custom': 1}, {'custom': 2}]\n\n    async def test_type_error(self):\n        resp = Response(data='not a dict')\n        with self.assertRaises(TypeError):\n            await resp.serialize_output(OutputModelWithAlias)\n\n    async def test_model(self):\n        user = await User.insert_one(username='Ali', password='1234')\n        resp = Response(data=user)\n        await resp.serialize_output(UserOutputSerializer)\n        assert resp.data == {'id': user.id, 'username': 'Ali'}\n\n    async def test_to_response_instance_argument(self):\n        OutputModelWithInstanceCheck.called_with.clear()\n        resp = Response(data={'value': 123, 'etc': 'ok'})\n        await resp.serialize_output(OutputModelWithInstanceCheck)\n        # The instance should be the original data dict\n        assert resp.data == {'value': 123, 'instance_type': 'dict'}\n        assert OutputModelWithInstanceCheck.called_with[0][0] == {'value': 123, 'etc': 'ok'}\n        assert OutputModelWithInstanceCheck.called_with[0][1] == {'value': 123}\n\n    async def test_iterable_to_response_instance_argument(self):\n        OutputModelWithInstanceCheckIterable.called_with.clear()\n        data = [{'value': 1, 'etc': 'ok'}, {'value': 2, 'etc': 'ok'}]\n        resp = Response(data=data)\n        await resp.serialize_output(OutputModelWithInstanceCheckIterable)\n        # The instance should be the original list for each call\n        assert resp.data == [\n            {'value': 1, 'instance_type': 'dict'},\n            {'value': 2, 'instance_type': 'dict'},\n        ]\n        # Both calls should have the same instance (the dict)\n        assert OutputModelWithInstanceCheckIterable.called_with[0][0] == {'value': 1, 'etc': 'ok'}\n        assert OutputModelWithInstanceCheckIterable.called_with[1][0] == {'value': 2, 'etc': 'ok'}\n        assert OutputModelWithInstanceCheckIterable.called_with[0][1] == {'value': 1}\n        assert OutputModelWithInstanceCheckIterable.called_with[1][1] == {'value': 2}\n\n    async def test_model_instance_to_response_argument(self):\n        class UserOutputWithInstance(BaseModel):\n            id: str\n            username: str\n            called_with: ClassVar = []\n\n            async def to_response(self, instance, data):\n                UserOutputWithInstance.called_with.append((instance, data))\n                return {'id': data['id'], 'username': data['username'], 'instance_type': type(instance).__name__}\n\n        UserOutputWithInstance.called_with.clear()\n        user = await User.insert_one(username='Ali', password='1234')\n        resp = Response(data=user)\n        await resp.serialize_output(UserOutputWithInstance)\n        assert resp.data['id'] == user.id\n        assert resp.data['username'] == 'Ali'\n        assert resp.data['instance_type'] == 'User'\n        assert UserOutputWithInstance.called_with[0][0] is user\n        assert UserOutputWithInstance.called_with[0][1]['id'] == user.id\n"
  },
  {
    "path": "tests/test_routing.py",
    "content": "import random\nfrom unittest import TestCase\n\nfrom panther.base_request import BaseRequest\nfrom panther.configs import config\nfrom panther.exceptions import PantherError\nfrom panther.routings import (\n    ENDPOINT_NOT_FOUND,\n    finalize_urls,\n    find_endpoint,\n    flatten_urls,\n)\n\n\nclass TestRoutingFunctions(TestCase):\n    def tearDown(self) -> None:\n        config.refresh()\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n\n    # Collecting\n    def test_flatten_urls_ellipsis_endpoints(self):\n        urls1 = {\n            'user/': {\n                '<user_id>/': ...,\n            },\n        }\n        urls2 = {\n            'user/': {\n                'list/': ...,\n            },\n        }\n\n        try:\n            flatten_urls(urls1)\n        except PantherError as exc:\n            assert exc.args[0] == \"URL Can't Point To Ellipsis. ('user/<user_id>/' -> ...)\"\n        else:\n            assert False\n\n        try:\n            flatten_urls(urls2)\n        except PantherError as exc:\n            assert exc.args[0] == \"URL Can't Point To Ellipsis. ('user/list/' -> ...)\"\n        else:\n            assert False\n\n    def test_flatten_urls_none_endpoints(self):\n        urls1 = {\n            'user/': {\n                'list/': None,\n            },\n        }\n        urls2 = {\n            'user/': {\n                '<user_id>/': None,\n            },\n        }\n\n        try:\n            flatten_urls(urls1)\n        except PantherError as exc:\n            assert exc.args[0] == \"URL Can't Point To None. ('user/list/' -> None)\"\n        else:\n            assert False\n\n        try:\n            flatten_urls(urls2)\n        except PantherError as exc:\n            assert exc.args[0] == \"URL Can't Point To None. ('user/<user_id>/' -> None)\"\n        else:\n            assert False\n\n    def test_flatten_urls_invalid_url(self):\n        def temp_func():\n            pass\n\n        urls1 = {\n            'user/': {\n                '?': temp_func,\n            },\n        }\n        urls2 = {\n            'user/': {\n                'لیست': temp_func,\n            },\n        }\n\n        try:\n            flatten_urls(urls1)\n        except PantherError as exc:\n            assert exc.args[0] == \"URL Is Not Valid. --> 'user/?/'\"\n        else:\n            assert False\n\n        try:\n            flatten_urls(urls2)\n        except PantherError as exc:\n            assert exc.args[0] == \"URL Is Not Valid. --> 'user/لیست/'\"\n        else:\n            assert False\n\n    def test_flatten_urls_empty_url(self):\n        def temp_func():\n            pass\n\n        urls = {\n            '': temp_func,\n        }\n\n        collected_urls = flatten_urls(urls)\n\n        expected_result = {\n            '': temp_func,\n        }\n        assert collected_urls == expected_result\n\n    def test_flatten_urls_nested_empty_urls(self):\n        def temp_func():\n            pass\n\n        urls = {\n            '': {\n                '': {\n                    'nested/<user_id>/': temp_func,\n                    'nested/profile/': temp_func,\n                    'nested/list/': temp_func,\n                },\n                '<user_id>/': temp_func,\n                'profile/': temp_func,\n                'list/': temp_func,\n            },\n        }\n\n        collected_urls = flatten_urls(urls)\n\n        expected_result = {\n            '<user_id>/': temp_func,\n            'profile/': temp_func,\n            'list/': temp_func,\n            'nested/<user_id>/': temp_func,\n            'nested/profile/': temp_func,\n            'nested/list/': temp_func,\n        }\n        assert collected_urls == expected_result\n\n    def test_flatten_urls_slash_url(self):\n        def temp_func():\n            pass\n\n        urls = {\n            '/': temp_func,\n        }\n\n        collected_urls = flatten_urls(urls)\n\n        expected_result = {\n            '/': temp_func,\n        }\n        assert collected_urls == expected_result\n\n    def test_flatten_urls_nested_slash_urls(self):\n        def temp_func():\n            pass\n\n        urls = {\n            '/': {\n                '/': {\n                    'nested/<user_id>/': temp_func,\n                    'nested/profile/': temp_func,\n                    'nested/list/': temp_func,\n                },\n                '<user_id>/': temp_func,\n                'profile/': temp_func,\n                'list/': temp_func,\n            },\n        }\n\n        collected_urls = flatten_urls(urls)\n\n        expected_result = {\n            '/<user_id>/': temp_func,\n            '/profile/': temp_func,\n            '/list/': temp_func,\n            '//nested/<user_id>/': temp_func,\n            '//nested/profile/': temp_func,\n            '//nested/list/': temp_func,\n        }\n        assert collected_urls == expected_result\n\n    def test_flatten_urls_simple_urls(self):\n        def temp_func():\n            pass\n\n        urls = {\n            '<user_id>/': temp_func,\n            'profile/': temp_func,\n            'list/': temp_func,\n        }\n\n        collected_urls = flatten_urls(urls)\n\n        expected_result = {\n            '<user_id>/': temp_func,\n            'profile/': temp_func,\n            'list/': temp_func,\n        }\n        assert collected_urls == expected_result\n\n    def test_flatten_urls_simple_nested_urls(self):\n        def temp_func():\n            pass\n\n        urls = {\n            'user/': {\n                '<user_id>/': temp_func,\n                'profile/': temp_func,\n                'list/': temp_func,\n            },\n        }\n\n        collected_urls = flatten_urls(urls)\n\n        expected_result = {\n            'user/<user_id>/': temp_func,\n            'user/profile/': temp_func,\n            'user/list/': temp_func,\n        }\n        assert collected_urls == expected_result\n\n    def test_flatten_urls_simple_nested_urls_without_slash_at_end(self):\n        def temp_func():\n            pass\n\n        urls = {\n            'user': {\n                '<user_id>': temp_func,\n                'profile': temp_func,\n                'list': temp_func,\n            },\n        }\n\n        collected_urls = flatten_urls(urls)\n\n        expected_result = {\n            'user/<user_id>/': temp_func,\n            'user/profile/': temp_func,\n            'user/list/': temp_func,\n        }\n        assert collected_urls == expected_result\n\n    def test_flatten_urls_complex_nested_urls(self):\n        def temp_func():\n            pass\n\n        urls = {\n            'user/': {\n                '<user_id>/profile/<id>': temp_func,\n                'profile/': temp_func,\n                'list/': temp_func,\n            },\n            '': {\n                'payments': temp_func,\n                'notifications': temp_func,\n            },\n            'admin/v1': {\n                'profile/avatar': temp_func,\n                '<user_id>': temp_func,\n                'users/': {\n                    'list': {\n                        'registered': temp_func,\n                        'not-registered': temp_func,\n                    },\n                    'detail': {\n                        'registered': temp_func,\n                        'not-registered': temp_func,\n                    },\n                },\n            },\n            'admin/v2': {},\n        }\n\n        collected_urls = flatten_urls(urls)\n        expected_result = {\n            'user/<user_id>/profile/<id>/': temp_func,\n            'user/profile/': temp_func,\n            'user/list/': temp_func,\n            'payments/': temp_func,\n            'notifications/': temp_func,\n            'admin/v1/profile/avatar/': temp_func,\n            'admin/v1/<user_id>/': temp_func,\n            'admin/v1/users/list/registered/': temp_func,\n            'admin/v1/users/list/not-registered/': temp_func,\n            'admin/v1/users/detail/registered/': temp_func,\n            'admin/v1/users/detail/not-registered/': temp_func,\n            'admin/v2/': {},\n        }\n        assert collected_urls == expected_result\n\n    # Finalize\n    def test_finalize_empty_url(self):\n        def temp_func():\n            pass\n\n        urls = {\n            '': temp_func,\n        }\n\n        collected_urls = flatten_urls(urls)\n        finalized_urls = finalize_urls(collected_urls)\n\n        expected_result = {\n            '': temp_func,\n        }\n        assert finalized_urls == expected_result\n\n    def test_finalize_empty_url_nested(self):\n        def temp_func():\n            pass\n\n        urls = {\n            '': {\n                '': {\n                    'nested/<user_id>/': temp_func,\n                    'nested/profile/': temp_func,\n                    'nested/list/': temp_func,\n                },\n                '<user_id>/': temp_func,\n                'profile/': temp_func,\n                'list/': temp_func,\n            },\n        }\n\n        collected_urls = flatten_urls(urls)\n        finalized_urls = finalize_urls(collected_urls)\n\n        expected_result = {\n            'nested': {\n                '<user_id>': temp_func,\n                'profile': temp_func,\n                'list': temp_func,\n            },\n            '<user_id>': temp_func,\n            'profile': temp_func,\n            'list': temp_func,\n        }\n        assert finalized_urls == expected_result\n\n    def test_finalize_root_url(self):\n        def temp_func():\n            pass\n\n        urls = {\n            '/': temp_func,\n        }\n\n        collected_urls = flatten_urls(urls)\n        finalized_urls = finalize_urls(collected_urls)\n\n        expected_result = {\n            '': temp_func,\n        }\n        assert finalized_urls == expected_result\n\n    def test_finalize_root_url_nested(self):\n        def temp_func():\n            pass\n\n        urls = {\n            '/': {\n                '/': {\n                    'nested/<user_id>/': temp_func,\n                    'nested/profile/': temp_func,\n                    'nested/list/': temp_func,\n                },\n                '<user_id>/': temp_func,\n                'profile/': temp_func,\n                'list/': temp_func,\n            },\n        }\n\n        collected_urls = flatten_urls(urls)\n        finalized_urls = finalize_urls(collected_urls)\n\n        expected_result = {\n            'nested': {\n                '<user_id>': temp_func,\n                'profile': temp_func,\n                'list': temp_func,\n            },\n            '<user_id>': temp_func,\n            'profile': temp_func,\n            'list': temp_func,\n        }\n        assert finalized_urls == expected_result\n\n    def test_finalize_root_and_empty_url_nested(self):\n        def temp_func():\n            pass\n\n        urls = {\n            '/': {\n                '': {\n                    'nested/<user_id>/': temp_func,\n                    'nested/profile/': temp_func,\n                    'nested/list/': temp_func,\n                },\n                '/': temp_func,\n                'profile/': temp_func,\n                'list/': temp_func,\n            },\n        }\n\n        collected_urls = flatten_urls(urls)\n        finalized_urls = finalize_urls(collected_urls)\n\n        expected_result = {\n            'nested': {\n                '<user_id>': temp_func,\n                'profile': temp_func,\n                'list': temp_func,\n            },\n            '': temp_func,\n            'profile': temp_func,\n            'list': temp_func,\n        }\n        assert finalized_urls == expected_result\n\n    def test_finalize_empty_and_root_url_nested(self):\n        def temp_func():\n            pass\n\n        urls = {\n            '': {\n                '/': {\n                    'nested/<user_id>/': temp_func,\n                    'nested/profile/': temp_func,\n                    'nested/list/': temp_func,\n                },\n                '': temp_func,\n                'profile/': temp_func,\n                'list/': temp_func,\n            },\n        }\n\n        collected_urls = flatten_urls(urls)\n        finalized_urls = finalize_urls(collected_urls)\n\n        expected_result = {\n            'nested': {\n                '<user_id>': temp_func,\n                'profile': temp_func,\n                'list': temp_func,\n            },\n            '': temp_func,\n            'profile': temp_func,\n            'list': temp_func,\n        }\n        assert finalized_urls == expected_result\n\n    def test_finalize_urls(self):\n        def temp_func():\n            pass\n\n        urls = {\n            'user/': {\n                '<user_id>/profile/<id>': temp_func,\n                'profile/': temp_func,\n                'list/': temp_func,\n            },\n            '': {\n                'payments': temp_func,\n                'notifications': temp_func,\n            },\n            'admin/v1': {\n                'profile/avatar': temp_func,\n                '<user_id>': temp_func,\n                'users/': {\n                    'list': {\n                        'registered': temp_func,\n                        'not-registered': temp_func,\n                    },\n                    'detail': {\n                        'registered': temp_func,\n                        'not-registered': temp_func,\n                    },\n                },\n            },\n            'admin/v2': {},\n        }\n\n        collected_urls = flatten_urls(urls)\n        finalized_urls = finalize_urls(collected_urls)\n\n        expected_result = {\n            'user': {\n                '<user_id>': {\n                    'profile': {\n                        '<id>': temp_func,\n                    },\n                },\n                'profile': temp_func,\n                'list': temp_func,\n            },\n            'payments': temp_func,\n            'notifications': temp_func,\n            'admin': {\n                'v1': {\n                    'profile': {\n                        'avatar': temp_func,\n                    },\n                    '<user_id>': temp_func,\n                    'users': {\n                        'list': {\n                            'registered': temp_func,\n                            'not-registered': temp_func,\n                        },\n                        'detail': {\n                            'registered': temp_func,\n                            'not-registered': temp_func,\n                        },\n                    },\n                },\n                'v2': {},\n            },\n        }\n        assert finalized_urls == expected_result\n\n    def test_finalize_urls_same_pre_path_variable(self):\n        def temp_func():\n            pass\n\n        urls = {\n            '': temp_func,\n            '<index>/': temp_func,\n            '<index>/<id>/': temp_func,\n        }\n\n        collected_urls = flatten_urls(urls)\n        finalized_urls = finalize_urls(collected_urls)\n        expected_result = {\n            '': temp_func,\n            '<index>': {\n                '': temp_func,\n                '<id>': temp_func,\n            },\n        }\n        assert finalized_urls == expected_result\n\n    def test_finalize_urls_with_same_level_path_variables(self):\n        def temp_func():\n            pass\n\n        urls1 = {\n            'user': {\n                '<index1>/': temp_func,\n                '<index2>/': temp_func,\n            },\n        }\n        urls2 = {\n            'user': {\n                '<index1>/': {'detail': temp_func},\n                '<index2>/': temp_func,\n                '<index3>/': {'detail': temp_func},\n                '<index4>/': {'detail': temp_func},\n            },\n        }\n\n        try:\n            finalize_urls(flatten_urls(urls1))\n        except PantherError as exc:\n            assert exc.args[0] == (\n                \"URLs can't have same-level path variables that point to an endpoint: \"\n                '\\n\\t- /user/<index1>'\n                '\\n\\t- /user/<index2>'\n            )\n        else:\n            assert False\n\n        try:\n            finalize_urls(flatten_urls(urls2))\n        except PantherError as exc:\n            assert exc.args[0] == (\n                \"URLs can't have same-level path variables that point to a dict: \"\n                '\\n\\t- /user/<index1>'\n                '\\n\\t- /user/<index3>'\n                '\\n\\t- /user/<index4>'\n            )\n        else:\n            assert False\n\n    # Find Endpoint\n    def test_find_endpoint_root_url(self):\n        def temp_func():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            '': temp_func,\n        }\n        _func, _ = find_endpoint('')\n\n        assert _func == temp_func\n\n    def test_find_endpoint_success(self):\n        def user_id_profile_id():\n            pass\n\n        def user_profile():\n            pass\n\n        def payment():\n            pass\n\n        def admin_v1_profile_avatar():\n            pass\n\n        def admin_v1_id():\n            pass\n\n        def admin_v2_users_list_registered():\n            pass\n\n        def admin_v2_users_detail_not_registered():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            'user': {\n                '<user_id>': {\n                    'profile': {\n                        '<id>': user_id_profile_id,\n                    },\n                },\n                'profile': user_profile,\n                'list': ...,\n            },\n            'payments': payment,\n            'notifications': ...,\n            'admin': {\n                'v1': {\n                    'profile': {\n                        'avatar': admin_v1_profile_avatar,\n                    },\n                    '<user_id>': admin_v1_id,\n                    'users': {\n                        'list': {\n                            'registered': admin_v2_users_list_registered,\n                            'not-registered': ...,\n                        },\n                        'detail': {\n                            'registered': ...,\n                            'not-registered': admin_v2_users_detail_not_registered,\n                        },\n                    },\n                },\n            },\n        }\n        user_id_profile_id_func, _ = find_endpoint(f'user/{random.randint(0, 100)}/profile/{random.randint(2, 100)}')\n        user_profile_func, _ = find_endpoint('user/profile/')\n        payment_func, _ = find_endpoint('payments/')\n        admin_v1_profile_avatar_func, _ = find_endpoint('admin/v1/profile/avatar')\n        admin_v1_id_func, _ = find_endpoint(f'admin/v1/{random.randint(0, 100)}')\n        admin_v2_users_list_registered_func, _ = find_endpoint('admin/v1/users/list/registered/')\n        admin_v2_users_detail_not_registered_func, _ = find_endpoint('admin/v1/users/detail/not-registered')\n\n        assert user_id_profile_id_func == user_id_profile_id\n        assert user_profile_func == user_profile\n        assert payment_func == payment\n        assert admin_v1_profile_avatar_func == admin_v1_profile_avatar\n        assert admin_v1_id_func == admin_v1_id\n        assert admin_v2_users_list_registered_func == admin_v2_users_list_registered\n        assert admin_v2_users_detail_not_registered_func == admin_v2_users_detail_not_registered\n\n    def test_find_endpoint_success_path(self):\n        def user_id_profile_id():\n            pass\n\n        def user_profile():\n            pass\n\n        def payment():\n            pass\n\n        def admin_v1_profile_avatar():\n            pass\n\n        def admin_v1_id():\n            pass\n\n        def admin_v2_users_list_registered():\n            pass\n\n        def admin_v2_users_detail_not_registered():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            'user': {\n                '<user_id>': {\n                    'profile': {\n                        '<id>': user_id_profile_id,\n                    },\n                },\n                'profile': user_profile,\n                'list': ...,\n            },\n            'payments': payment,\n            'notifications': ...,\n            'admin': {\n                'v1': {\n                    'profile': {\n                        'avatar': admin_v1_profile_avatar,\n                    },\n                    '<user_id>': admin_v1_id,\n                    '<user_id2>': {\n                        'list': {\n                            '<registered2>': {},\n                            '<registered1>': admin_v2_users_list_registered,\n                            'registered': admin_v2_users_list_registered,\n                        },\n                    },\n                    'users': {\n                        'list': {\n                            'registered': admin_v2_users_list_registered,\n                            'not-registered': ...,\n                        },\n                        'detail': {\n                            'registered': ...,\n                            'not-registered': admin_v2_users_detail_not_registered,\n                        },\n                    },\n                },\n            },\n        }\n        _, user_id_profile_id_path = find_endpoint(f'user/{random.randint(0, 100)}/profile/{random.randint(2, 100)}')\n        _, user_profile_path = find_endpoint('user/profile/')\n        _, payment_path = find_endpoint('payments/')\n        _, admin_v1_profile_avatar_path = find_endpoint('admin/v1/profile/avatar')\n        _, admin_v1_id_path = find_endpoint(f'admin/v1/{random.randint(0, 100)}')\n        _, admin_v1_id_registered_path = find_endpoint(f'admin/v1/{random.randint(0, 100)}/list/registered')\n        _, admin_v1_id_registered1_path = find_endpoint(f'admin/v1/{random.randint(0, 100)}/list/1/')\n        _, admin_v2_users_list_registered_path = find_endpoint('admin/v1/users/list/registered/')\n        _, admin_v2_users_detail_not_registered_path = find_endpoint('admin/v1/users/detail/not-registered')\n\n        assert user_id_profile_id_path == 'user/<user_id>/profile/<id>'\n        assert user_profile_path == 'user/profile'\n        assert payment_path == 'payments'\n        assert admin_v1_profile_avatar_path == 'admin/v1/profile/avatar'\n        assert admin_v1_id_path == 'admin/v1/<user_id>'\n        assert admin_v1_id_registered_path == 'admin/v1/<user_id2>/list/registered'\n        assert admin_v1_id_registered1_path == 'admin/v1/<user_id2>/list/<registered1>'\n        assert admin_v2_users_list_registered_path == 'admin/v1/users/list/registered'\n        assert admin_v2_users_detail_not_registered_path == 'admin/v1/users/detail/not-registered'\n\n    def test_find_endpoint_not_found(self):\n        def temp_func():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            'user': {\n                'list': temp_func,\n            },\n        }\n        user_id_profile_id_func, _ = find_endpoint(f'user/{random.randint(0, 100)}/profile/{random.randint(2, 100)}')\n        user_profile_func, _ = find_endpoint('user/profile/')\n        payment_func, _ = find_endpoint('payments/')\n        admin_v1_profile_avatar_func, _ = find_endpoint('admin/v1/profile/avatar')\n        admin_v1_id_func, _ = find_endpoint(f'admin/v1/{random.randint(0, 100)}')\n        admin_v2_users_list_registered_func, _ = find_endpoint('admin/v1/users/list/registered/')\n        admin_v2_users_detail_not_registered_func, _ = find_endpoint('admin/v1/users/detail/not-registered')\n\n        assert user_id_profile_id_func is None\n        assert user_profile_func is None\n        assert payment_func is None\n        assert admin_v1_profile_avatar_func is None\n        assert admin_v1_id_func is None\n        assert admin_v2_users_list_registered_func is None\n        assert admin_v2_users_detail_not_registered_func is None\n\n    def test_find_endpoint_not_found_path(self):\n        def temp_func():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            'user': {\n                'list': temp_func,\n            },\n        }\n        _, user_id_profile_id_path = find_endpoint(f'user/{random.randint(0, 100)}/profile/{random.randint(2, 100)}')\n        _, user_profile_path = find_endpoint('user/profile/')\n        _, payment_path = find_endpoint('payments/')\n        _, admin_v1_profile_avatar_path = find_endpoint('admin/v1/profile/avatar')\n        _, admin_v1_id_path = find_endpoint(f'admin/v1/{random.randint(0, 100)}')\n        _, admin_v2_users_list_registered_path = find_endpoint('admin/v1/users/list/registered/')\n        _, admin_v2_users_detail_not_registered_path = find_endpoint('admin/v1/users/detail/not-registered')\n\n        assert user_id_profile_id_path == ''\n        assert user_profile_path == ''\n        assert payment_path == ''\n        assert admin_v1_profile_avatar_path == ''\n        assert admin_v1_id_path == ''\n        assert admin_v2_users_list_registered_path == ''\n        assert admin_v2_users_detail_not_registered_path == ''\n\n    def test_find_endpoint_not_found_last_is_path_variable(self):\n        def temp_func():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            'user': {\n                '<name>': temp_func,\n            },\n        }\n        user_id_profile_id_func, _ = find_endpoint(f'user/{random.randint(0, 100)}/profile/{random.randint(2, 100)}')\n        user_profile_func, _ = find_endpoint('user/ali/')\n        payment_func, _ = find_endpoint('payments/')\n        admin_v1_profile_avatar_func, _ = find_endpoint('admin/v1/profile/avatar')\n        admin_v1_id_func, _ = find_endpoint(f'admin/v1/{random.randint(0, 100)}')\n        admin_v2_users_list_registered_func, _ = find_endpoint('admin/v1/users/list/registered/')\n        admin_v2_users_detail_not_registered_func, _ = find_endpoint('admin/v1/users/detail/not-registered')\n\n        assert user_id_profile_id_func is None\n        assert user_profile_func is not None\n        assert payment_func is None\n        assert admin_v1_profile_avatar_func is None\n        assert admin_v1_id_func is None\n        assert admin_v2_users_list_registered_func is None\n        assert admin_v2_users_detail_not_registered_func is None\n\n    def test_find_endpoint_not_found_path_last_is_path_variable(self):\n        def temp_func():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            'user': {\n                '<name>': temp_func,\n            },\n        }\n        _, user_id_profile_id_path = find_endpoint(f'user/{random.randint(0, 100)}/profile/{random.randint(2, 100)}')\n        _, user_profile_path = find_endpoint('user/ali/')\n        _, payment_path = find_endpoint('payments/')\n        _, admin_v1_profile_avatar_path = find_endpoint('admin/v1/profile/avatar')\n        _, admin_v1_id_path = find_endpoint(f'admin/v1/{random.randint(0, 100)}')\n        _, admin_v2_users_list_registered_path = find_endpoint('admin/v1/users/list/registered/')\n        _, admin_v2_users_detail_not_registered_path = find_endpoint('admin/v1/users/detail/not-registered')\n\n        assert user_id_profile_id_path == ''\n        assert user_profile_path != ''\n        assert payment_path == ''\n        assert admin_v1_profile_avatar_path == ''\n        assert admin_v1_id_path == ''\n        assert admin_v2_users_list_registered_path == ''\n        assert admin_v2_users_detail_not_registered_path == ''\n\n    def test_find_endpoint_not_found_too_many(self):\n        def temp_func():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            'user/name': temp_func,\n        }\n        func, path = find_endpoint('user/name/troublemaker')\n\n        assert path == ''\n        assert func is None\n\n    def test_find_endpoint_not_found_not_enough(self):\n        def temp_func():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            'user/name': temp_func,\n        }\n        func, path = find_endpoint('user/')\n\n        assert path == ''\n        assert func is None\n\n    def test_find_endpoint_same_pre_path_variable(self):\n        def temp_1():\n            pass\n\n        def temp_2():\n            pass\n\n        def temp_3():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            '': temp_1,\n            '<index>': {\n                '': temp_2,\n                '<id>': temp_3,\n            },\n        }\n        temp_1_func, _ = find_endpoint('')\n        temp_2_func, _ = find_endpoint(f'{random.randint(2, 100)}')\n        temp_3_func, _ = find_endpoint(f'{random.randint(2, 100)}/{random.randint(2, 100)}')\n\n        assert temp_1_func == temp_1\n        assert temp_2_func == temp_2\n        assert temp_3_func == temp_3\n\n    def test_find_endpoint_same_pre_path_variable_path(self):\n        def temp_1():\n            pass\n\n        def temp_2():\n            pass\n\n        def temp_3():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            '': temp_1,\n            '<index>': {\n                '': temp_2,\n                '<id>': temp_3,\n            },\n        }\n        _, temp_1_path = find_endpoint('')\n        _, temp_2_path = find_endpoint(f'{random.randint(2, 100)}')\n        _, temp_3_path = find_endpoint(f'{random.randint(2, 100)}/{random.randint(2, 100)}')\n\n        assert temp_1_path == ''\n        assert temp_2_path == '<index>'\n        assert temp_3_path == '<index>/<id>'\n\n    def test_find_endpoint_same_pre_key(self):\n        def temp_1():\n            pass\n\n        def temp_2():\n            pass\n\n        def temp_3():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            '': temp_1,\n            'hello': {\n                '': temp_2,\n                '<id>': temp_3,\n            },\n        }\n        temp_1_func, _ = find_endpoint('')\n\n        temp_2_func, _ = find_endpoint('hello')\n        temp_3_func, _ = find_endpoint(f'hello/{random.randint(2, 100)}')\n\n        assert temp_1_func == temp_1\n        assert temp_2_func == temp_2\n        assert temp_3_func == temp_3\n\n    def test_find_endpoint_same_pre_key_path(self):\n        def temp_1():\n            pass\n\n        def temp_2():\n            pass\n\n        def temp_3():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            '': temp_1,\n            'hello': {\n                '': temp_2,\n                '<id>': temp_3,\n            },\n        }\n        _, temp_1_path = find_endpoint('')\n\n        _, temp_2_path = find_endpoint('hello')\n        _, temp_3_path = find_endpoint(f'hello/{random.randint(2, 100)}')\n\n        assert temp_1_path == ''\n        assert temp_2_path == 'hello'\n        assert temp_3_path == 'hello/<id>'\n\n    def test_find_endpoint_with_params(self):\n        def user_id_profile_id():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            'user': {\n                '<user_id>': {\n                    'profile': user_id_profile_id,\n                },\n            },\n        }\n        user_id_profile_id_func, _ = find_endpoint(f'user/{random.randint(0, 100)}/profile?name=ali')\n\n        assert user_id_profile_id_func == user_id_profile_id\n\n    # Collect PathVariables\n    def test_collect_path_variables(self):\n        def temp_func():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            'user': {\n                '<user_id>': {\n                    'profile': {\n                        '<id>': temp_func,\n                    },\n                },\n            },\n        }\n\n        _user_id = random.randint(0, 100)\n        _id = random.randint(0, 100)\n        request_path = f'user/{_user_id}/profile/{_id}'\n\n        _, found_path = find_endpoint(request_path)\n        request = BaseRequest(scope={'path': request_path}, receive=lambda x: x, send=lambda x: x)\n        request.collect_path_variables(found_path=found_path)\n        path_variables = request.path_variables\n\n        assert isinstance(path_variables, dict)\n\n        assert 'user_id' in path_variables\n        assert 'id' in path_variables\n\n        assert path_variables['user_id'] == str(_user_id)\n        assert path_variables['id'] == str(_id)\n\n    # Complete test ready for benchmarking\n    def test_find_endpoint_complete(self):\n        def _():\n            pass\n\n        def _3():\n            pass\n\n        def _1():\n            pass\n\n        def _4():\n            pass\n\n        def _159():\n            pass\n\n        def _16():\n            pass\n\n        def _1710():\n            pass\n\n        def _18():\n            pass\n\n        def _19():\n            pass\n\n        def _211():\n            pass\n\n        def _2121516():\n            pass\n\n        def _2141718():\n            pass\n\n        def _220():\n            pass\n\n        def _22122():\n            pass\n\n        from panther.configs import config\n\n        config.URLS = {\n            '': _,\n            '0': {'21': {}},\n            '1': {\n                '': _1,\n                '<5>': {'<9>': _159},\n                '6': _16,\n                '7': {'10': _1710},\n                '<8>': _18,\n            },\n            '<2>': {\n                '11': _211,\n                '12': {'15': {'16': _2121516}},\n                '<14>': {'<17>': {'<18>': _2141718}},\n                '19': {'': _19},\n                '<20>': _220,\n                '21': {'<22>': {'': _22122}},\n            },\n            '3': _3,\n            '<4>': _4,\n        }\n\n        test_cases = {\n            '': (_, ''),\n            '0': ENDPOINT_NOT_FOUND,\n            '0/21': ENDPOINT_NOT_FOUND,\n            '3': (_3, '3'),\n            '_4': (_4, '<4>'),\n            '1': (_1, '1'),\n            '1/_5/_9': (_159, '1/<5>/<9>'),\n            '1/6': (_16, '1/6'),\n            '1/7/10': (_1710, '1/7/10'),\n            '1/7': ENDPOINT_NOT_FOUND,\n            '1/_8': (_18, '1/<8>'),\n            '_2/11': (_211, '<2>/11'),\n            '_2/12/15/16': (_2121516, '<2>/12/15/16'),\n            '_2/_14/_17/_18': (_2141718, '<2>/<14>/<17>/<18>'),\n            '_2/19': (_19, '<2>/19'),\n            '_2/_20': (_220, '<2>/<20>'),\n            '_2/21/_22': (_22122, '<2>/21/<22>'),\n            # # #\n            '/': (_, ''),\n            '/0/': ENDPOINT_NOT_FOUND,\n            '/0/21/': ENDPOINT_NOT_FOUND,\n            '/3/': (_3, '3'),\n            '/_4/': (_4, '<4>'),\n            '/1/': (_1, '1'),\n            '/1/_5/_9/': (_159, '1/<5>/<9>'),\n            '/1/6/': (_16, '1/6'),\n            '/1/7/10/': (_1710, '1/7/10'),\n            '/1/7/': ENDPOINT_NOT_FOUND,\n            '/1/_8/': (_18, '1/<8>'),\n            '/_2/11/': (_211, '<2>/11'),\n            '/_2/12/15/16/': (_2121516, '<2>/12/15/16'),\n            '/_2/_14/_17/_18/': (_2141718, '<2>/<14>/<17>/<18>'),\n            '/_2/_14/_17/': ENDPOINT_NOT_FOUND,\n            '/_2/19/': (_19, '<2>/19'),\n            '/_2/_20/': (_220, '<2>/<20>'),\n            '/_2/21/_22/': (_22122, '<2>/21/<22>'),\n        }\n\n        for test_url, expected in test_cases.items():\n            actual = find_endpoint(path=test_url)\n            assert actual == expected\n"
  },
  {
    "path": "tests/test_run.py",
    "content": "import sys\nfrom datetime import timedelta\nfrom pathlib import Path\nfrom unittest import TestCase\n\nimport tests.sample_project.app.models\nfrom panther import Panther\nfrom panther.configs import Config, config\n\n\nclass TestRun(TestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        sys.path.append('tests/sample_project')\n\n    @classmethod\n    def tearDownClass(cls) -> None:\n        config.refresh()\n        sys.path.pop()\n\n    def tearDown(self) -> None:\n        config.refresh()\n\n    def test_init(self):\n        app = Panther(__name__)\n        assert isinstance(app, Panther)\n\n    def test_load_configs(self):\n        base_dir = Path(__name__).resolve().parent\n        secret_key = 'fHrIYx3yK0J_UG0K0zD6miLPNy1esoYXzVsvif6e7rY='\n        Panther(__name__)\n\n        assert isinstance(config, Config)\n        assert base_dir == config.BASE_DIR\n        assert config.LOG_QUERIES is True\n        assert config.THROTTLING.rate == 10\n        assert config.THROTTLING.duration == timedelta(seconds=10)\n        assert secret_key == config.SECRET_KEY\n        assert len(config.HTTP_MIDDLEWARES) == 1\n        assert len(config.WS_MIDDLEWARES) == 0\n\n        assert config.QUERY_ENGINE.__name__ == 'BasePantherDBQuery'\n        assert config.USER_MODEL.__name__ == tests.sample_project.app.models.User.__name__\n        assert config.USER_MODEL.__module__.endswith('app.models')\n        assert config.JWT_CONFIG.algorithm == 'HS256'\n        assert config.JWT_CONFIG.life_time == int(timedelta(days=2).total_seconds())\n        assert config.JWT_CONFIG.key == secret_key\n\n        assert '' in config.URLS\n        config.URLS.pop('')\n        assert 'second' in config.URLS\n        config.URLS.pop('second')\n        assert config.URLS == {}\n"
  },
  {
    "path": "tests/test_serializer.py",
    "content": "from pathlib import Path\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom pydantic import ConfigDict, Field, field_validator\n\nfrom panther import Panther\nfrom panther.app import API\nfrom panther.configs import config\nfrom panther.db import Model\nfrom panther.db.connections import db\nfrom panther.request import Request\nfrom panther.serializer import ModelSerializer\nfrom panther.test import APIClient\n\nDB_PATH = 'test_db.pdb'\nDATABASE = {\n    'engine': {'class': 'panther.db.connections.PantherDBConnection', 'path': DB_PATH},\n}\n\n\nclass Book(Model):\n    name: str\n    author: str = Field('default_author')\n    pages_count: int = Field(0)\n\n\nclass Author(Model):\n    name: str\n    book: Book\n\n\nclass NotRequiredFieldsSerializer(ModelSerializer):\n    class Config:\n        model = Book\n        fields = ['author', 'pages_count']\n\n\nclass RequiredFieldsSerializer(ModelSerializer):\n    class Config:\n        model = Book\n        fields = ['name', 'author', 'pages_count']\n\n\nclass OnlyRequiredFieldsSerializer(ModelSerializer):\n    class Config:\n        model = Book\n        fields = ['name', 'author', 'pages_count']\n        required_fields = ['author', 'pages_count']\n\n\nclass WithValidatorsSerializer(ModelSerializer):\n    class Config:\n        model = Book\n        fields = ['name', 'author', 'pages_count']\n        required_fields = ['author', 'pages_count']\n\n    @field_validator('name', 'author', 'pages_count')\n    def validate_other(cls, field):\n        return 'validated'\n\n    @field_validator('pages_count')\n    def validate_pages_count(cls, field):\n        return 100\n\n\nclass WithClassFieldsSerializer(ModelSerializer):\n    age: int = Field(10)\n\n    class Config:\n        model = Book\n        fields = ['name', 'author', 'pages_count']\n        required_fields = ['author', 'pages_count']\n\n\nclass WithForeignKeySerializer(ModelSerializer):\n    class Config:\n        model = Author\n        fields = ['name', 'book']\n\n\n@API(input_model=NotRequiredFieldsSerializer)\nasync def not_required(request: Request):\n    return request.validated_data\n\n\n@API(input_model=RequiredFieldsSerializer)\nasync def required(request: Request):\n    return request.validated_data\n\n\n@API(input_model=OnlyRequiredFieldsSerializer)\nasync def only_required(request: Request):\n    return request.validated_data\n\n\n@API(input_model=WithValidatorsSerializer)\nasync def with_validators(request: Request):\n    return request.validated_data\n\n\n@API(input_model=WithClassFieldsSerializer)\nasync def with_class_fields(request: Request):\n    return request.validated_data\n\n\n@API(input_model=WithForeignKeySerializer)\nasync def with_foreign_key(request: Request):\n    return request.validated_data\n\n\nurls = {\n    'not-required': not_required,\n    'required': required,\n    'only-required': only_required,\n    'with-validators': with_validators,\n    'class-fields': with_class_fields,\n    'foreign-key': with_foreign_key,\n}\n\n\nclass TestModelSerializer(IsolatedAsyncioTestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        app = Panther(__name__, configs=__name__, urls=urls)\n        cls.client = APIClient(app=app)\n\n    def tearDown(self) -> None:\n        db.session.collection('Book').drop()\n        db.session.collection('Author').drop()\n\n    @classmethod\n    def tearDownClass(cls) -> None:\n        config.refresh()\n        Path(DB_PATH).unlink(missing_ok=True)\n\n    # # # Class Usage\n\n    async def test_not_required_fields_empty_response(self):\n        payload = {}\n        res = await self.client.post('not-required', payload=payload)\n        assert res.status_code == 200\n        assert res.data == {'author': 'default_author', 'pages_count': 0}\n\n    async def test_not_required_fields_full_response(self):\n        payload = {'author': 'ali', 'pages_count': '12'}\n        res = await self.client.post('not-required', payload=payload)\n        assert res.status_code == 200\n        assert res.data == {'author': 'ali', 'pages_count': 12}\n\n    async def test_required_fields_error(self):\n        payload = {}\n        res = await self.client.post('required', payload=payload)\n        assert res.status_code == 400\n        assert res.data == {'name': 'Field required'}\n\n    async def test_required_fields_success(self):\n        payload = {'name': 'how to code', 'author': 'ali', 'pages_count': '12'}\n        res = await self.client.post('required', payload=payload)\n        assert res.status_code == 200\n        assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12}\n\n    async def test_only_required_fields_error(self):\n        payload = {}\n        res = await self.client.post('only-required', payload=payload)\n        assert res.status_code == 400\n        assert res.data == {'name': 'Field required', 'author': 'Field required', 'pages_count': 'Field required'}\n\n    async def test_only_required_fields_success(self):\n        payload = {'name': 'how to code', 'author': 'ali', 'pages_count': '12'}\n        res = await self.client.post('only-required', payload=payload)\n        assert res.status_code == 200\n        assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12}\n\n    async def test_with_validators(self):\n        payload = {'name': 'how to code', 'author': 'ali', 'pages_count': '12'}\n        res = await self.client.post('with-validators', payload=payload)\n        assert res.status_code == 200\n        assert res.data == {'name': 'validated', 'author': 'validated', 'pages_count': 100}\n\n    async def test_with_class_fields_success(self):\n        # Test Default Value\n        payload1 = {'name': 'how to code', 'author': 'ali', 'pages_count': '12'}\n        res = await self.client.post('class-fields', payload=payload1)\n        assert res.status_code == 200\n        assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12, 'age': 10}\n\n        # Test Validation\n        payload2 = {'name': 'how to code', 'author': 'ali', 'pages_count': '12', 'age': 30}\n        res = await self.client.post('class-fields', payload=payload2)\n        assert res.status_code == 200\n        assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12, 'age': 30}\n\n    async def test_foreign_key(self):\n        book = await Book.insert_one(name='Book1', author='Ali', pages_count=2)\n        await Author.insert_one(name='AnotherAli', book=book)\n        payload = {'name': 'Something', 'book': book.id}\n        res = await self.client.post('foreign-key', payload=payload)\n        assert res.status_code == 200\n        assert res.data == {\n            'name': 'Something',\n            'book': {'id': book.id, 'name': 'Book1', 'author': 'Ali', 'pages_count': 2},\n        }\n\n    # # # Class Definition\n\n    async def test_define_class_without_meta(self):\n        try:\n\n            class Serializer0(ModelSerializer):\n                pass\n        except Exception as e:\n            assert isinstance(e, AttributeError)\n            assert e.args[0] == '`class Config` is required in tests.test_serializer.Serializer0.'\n        else:\n            assert False\n\n    async def test_define_class_without_model(self):\n        try:\n\n            class Serializer1(ModelSerializer):\n                class Config:\n                    pass\n        except Exception as e:\n            assert isinstance(e, AttributeError)\n            assert e.args[0] == '`Serializer1.Config.model` is required.'\n        else:\n            assert False\n\n    async def test_define_class_without_fields(self):\n        try:\n\n            class Serializer2(ModelSerializer):\n                class Config:\n                    model = Book\n        except Exception as e:\n            assert isinstance(e, AttributeError)\n            assert e.args[0] == '`Serializer2.Config.fields` is required.'\n        else:\n            assert False\n\n    async def test_define_class_with_invalid_fields(self):\n        try:\n\n            class Serializer3(ModelSerializer):\n                class Config:\n                    model = Book\n                    fields = ['ok', 'no']\n        except Exception as e:\n            assert isinstance(e, AttributeError)\n            assert e.args[0] == '`Serializer3.Config.fields.ok` is not in `Book.model_fields`'\n        else:\n            assert False\n\n    async def test_define_class_with_invalid_required_fields(self):\n        try:\n\n            class Serializer4(ModelSerializer):\n                class Config:\n                    model = Book\n                    fields = ['name', 'author']\n                    required_fields = ['pages_count']\n        except Exception as e:\n            assert isinstance(e, AttributeError)\n            assert e.args[0] == '`Serializer4.Config.required_fields.pages_count` is not defined in `Config.fields`.'\n        else:\n            assert False\n\n    async def test_define_class_with_invalid_model(self):\n        try:\n\n            class Serializer5(ModelSerializer):\n                class Config:\n                    model = ModelSerializer\n                    fields = ['name', 'author', 'pages_count']\n        except Exception as e:\n            assert isinstance(e, AttributeError)\n            assert (\n                e.args[0] == '`Serializer5.Config.model` is not subclass of `panther.db.Model` or `pydantic.BaseModel`.'\n            )\n        else:\n            assert False\n\n    async def test_define_class_with_invalid_exclude_1(self):\n        try:\n\n            class Serializer6(ModelSerializer):\n                class Config:\n                    model = Book\n                    fields = ['name', 'author', 'pages_count']\n                    exclude = ['not_found']\n\n        except Exception as e:\n            assert isinstance(e, AttributeError)\n            assert e.args[0] == '`Serializer6.Config.exclude.not_found` is not valid.'\n        else:\n            assert False\n\n    async def test_define_class_with_invalid_exclude_2(self):\n        try:\n\n            class Serializer7(ModelSerializer):\n                class Config:\n                    model = Book\n                    fields = ['name', 'pages_count']\n                    exclude = ['author']\n\n        except Exception as e:\n            assert isinstance(e, AttributeError)\n            assert e.args[0] == '`Serializer7.Config.exclude.author` is not defined in `Config.fields`.'\n        else:\n            assert False\n\n    async def test_with_star_fields_with_exclude3(self):\n        try:\n\n            class Serializer8(ModelSerializer):\n                class Config:\n                    model = Book\n                    fields = ['*']\n                    exclude = ['author']\n\n        except Exception as e:\n            assert isinstance(e, AttributeError)\n            assert e.args[0] == \"`Serializer8.Config.fields.*` is not valid. Did you mean `fields = '*'`\"\n        else:\n            assert False\n\n    # # # Serializer Usage\n    async def test_with_simple_model_config(self):\n        class Serializer(ModelSerializer):\n            model_config = ConfigDict(str_to_upper=True)\n\n            class Config:\n                model = Book\n                fields = ['name', 'author', 'pages_count']\n\n        serialized = Serializer(name='book', author='AliRn', pages_count='12')\n        assert serialized.name == 'BOOK'\n        assert serialized.author == 'ALIRN'\n        assert serialized.pages_count == 12\n\n    async def test_with_inner_model_config(self):\n        class Serializer(ModelSerializer):\n            class Config:\n                str_to_upper = True\n                model = Book\n                fields = ['name', 'author', 'pages_count']\n\n        serialized = Serializer(name='book', author='AliRn', pages_count='12')\n        assert serialized.name == 'BOOK'\n        assert serialized.author == 'ALIRN'\n        assert serialized.pages_count == 12\n\n    async def test_with_dual_model_config(self):\n        class Serializer(ModelSerializer):\n            model_config = ConfigDict(str_to_upper=False)\n\n            class Config:\n                str_to_upper = True\n                model = Book\n                fields = ['name', 'author', 'pages_count']\n\n        serialized = Serializer(name='book', author='AliRn', pages_count='12')\n        assert serialized.name == 'book'\n        assert serialized.author == 'AliRn'\n        assert serialized.pages_count == 12\n\n    async def test_serializer_doc(self):\n        class Serializer1(ModelSerializer):\n            \"\"\"Hello I'm Doc\"\"\"\n\n            class Config:\n                model = Book\n                fields = ['name', 'author', 'pages_count']\n\n        serialized = Serializer1(name='book', author='AliRn', pages_count='12')\n        assert serialized.__doc__ == \"Hello I'm Doc\"\n\n        class Serializer2(ModelSerializer):\n            class Config:\n                model = Book\n                fields = ['name', 'author', 'pages_count']\n\n        serialized = Serializer2(name='book', author='AliRn', pages_count='12')\n        assert serialized.__doc__ is None\n\n    async def test_with_exclude(self):\n        class Serializer(ModelSerializer):\n            class Config:\n                model = Book\n                fields = ['name', 'author', 'pages_count']\n                exclude = ['author']\n\n        serialized = Serializer(name='book', author='AliRn', pages_count='12')\n        assert set(serialized.model_dump().keys()) == {'name', 'pages_count'}\n        assert serialized.name == 'book'\n        assert serialized.pages_count == 12\n\n    async def test_with_star_fields(self):\n        class Serializer(ModelSerializer):\n            class Config:\n                model = Book\n                fields = '*'\n\n        serialized = Serializer(name='book', author='AliRn', pages_count='12')\n        assert set(serialized.model_dump().keys()) == {'id', 'name', 'author', 'pages_count'}\n        assert serialized.name == 'book'\n        assert serialized.author == 'AliRn'\n        assert serialized.pages_count == 12\n\n    async def test_with_star_fields_with_exclude(self):\n        class Serializer(ModelSerializer):\n            class Config:\n                model = Book\n                fields = '*'\n                exclude = ['author']\n\n        serialized = Serializer(name='book', author='AliRn', pages_count='12')\n        assert set(serialized.model_dump().keys()) == {'id', 'name', 'pages_count'}\n        assert serialized.name == 'book'\n        assert serialized.pages_count == 12\n"
  },
  {
    "path": "tests/test_status.py",
    "content": "from unittest import TestCase\n\nfrom panther import status\n\n\nclass TestStatus(TestCase):\n    @classmethod\n    def setUpClass(cls):\n        cls.statuses = {\n            'HTTP_100_CONTINUE': 100,\n            'HTTP_101_SWITCHING_PROTOCOLS': 101,\n            'HTTP_102_PROCESSING': 102,\n            'HTTP_103_EARLY_HINTS': 103,\n            'HTTP_200_OK': 200,\n            'HTTP_201_CREATED': 201,\n            'HTTP_202_ACCEPTED': 202,\n            'HTTP_203_NON_AUTHORITATIVE_INFORMATION': 203,\n            'HTTP_204_NO_CONTENT': 204,\n            'HTTP_205_RESET_CONTENT': 205,\n            'HTTP_206_PARTIAL_CONTENT': 206,\n            'HTTP_207_MULTI_STATUS': 207,\n            'HTTP_208_ALREADY_REPORTED': 208,\n            'HTTP_226_IM_USED': 226,\n            'HTTP_300_MULTIPLE_CHOICES': 300,\n            'HTTP_301_MOVED_PERMANENTLY': 301,\n            'HTTP_302_FOUND': 302,\n            'HTTP_303_SEE_OTHER': 303,\n            'HTTP_304_NOT_MODIFIED': 304,\n            'HTTP_305_USE_PROXY': 305,\n            'HTTP_306_RESERVED': 306,\n            'HTTP_307_TEMPORARY_REDIRECT': 307,\n            'HTTP_308_PERMANENT_REDIRECT': 308,\n            'HTTP_400_BAD_REQUEST': 400,\n            'HTTP_401_UNAUTHORIZED': 401,\n            'HTTP_402_PAYMENT_REQUIRED': 402,\n            'HTTP_403_FORBIDDEN': 403,\n            'HTTP_404_NOT_FOUND': 404,\n            'HTTP_405_METHOD_NOT_ALLOWED': 405,\n            'HTTP_406_NOT_ACCEPTABLE': 406,\n            'HTTP_407_PROXY_AUTHENTICATION_REQUIRED': 407,\n            'HTTP_408_REQUEST_TIMEOUT': 408,\n            'HTTP_409_CONFLICT': 409,\n            'HTTP_410_GONE': 410,\n            'HTTP_411_LENGTH_REQUIRED': 411,\n            'HTTP_412_PRECONDITION_FAILED': 412,\n            'HTTP_413_REQUEST_ENTITY_TOO_LARGE': 413,\n            'HTTP_414_REQUEST_URI_TOO_LONG': 414,\n            'HTTP_415_UNSUPPORTED_MEDIA_TYPE': 415,\n            'HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE': 416,\n            'HTTP_417_EXPECTATION_FAILED': 417,\n            'HTTP_418_IM_A_TEAPOT': 418,\n            'HTTP_421_MISDIRECTED_REQUEST': 421,\n            'HTTP_422_UNPROCESSABLE_ENTITY': 422,\n            'HTTP_423_LOCKED': 423,\n            'HTTP_424_FAILED_DEPENDENCY': 424,\n            'HTTP_425_TOO_EARLY': 425,\n            'HTTP_426_UPGRADE_REQUIRED': 426,\n            'HTTP_428_PRECONDITION_REQUIRED': 428,\n            'HTTP_429_TOO_MANY_REQUESTS': 429,\n            'HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE': 431,\n            'HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS': 451,\n            'HTTP_500_INTERNAL_SERVER_ERROR': 500,\n            'HTTP_501_NOT_IMPLEMENTED': 501,\n            'HTTP_502_BAD_GATEWAY': 502,\n            'HTTP_503_SERVICE_UNAVAILABLE': 503,\n            'HTTP_504_GATEWAY_TIMEOUT': 504,\n            'HTTP_505_HTTP_VERSION_NOT_SUPPORTED': 505,\n            'HTTP_506_VARIANT_ALSO_NEGOTIATES': 506,\n            'HTTP_507_INSUFFICIENT_STORAGE': 507,\n            'HTTP_508_LOOP_DETECTED': 508,\n            'HTTP_510_NOT_EXTENDED': 510,\n            'HTTP_511_NETWORK_AUTHENTICATION_REQUIRED': 511,\n            'WS_1000_NORMAL_CLOSURE': 1000,\n            'WS_1001_GOING_AWAY': 1001,\n            'WS_1002_PROTOCOL_ERROR': 1002,\n            'WS_1003_UNSUPPORTED_DATA': 1003,\n            'WS_1007_INVALID_FRAME_PAYLOAD_DATA': 1007,\n            'WS_1008_POLICY_VIOLATION': 1008,\n            'WS_1009_MESSAGE_TOO_BIG': 1009,\n            'WS_1010_MANDATORY_EXT': 1010,\n            'WS_1011_INTERNAL_ERROR': 1011,\n            'WS_1012_SERVICE_RESTART': 1012,\n            'WS_1013_TRY_AGAIN_LATER': 1013,\n            'WS_1014_BAD_GATEWAY': 1014,\n        }\n\n    def test_status_numbers(self):\n        for _status, number in self.statuses.items():\n            assert getattr(status, _status) == number\n\n    def test_status_is_informational(self):\n        for number in range(100, 600):\n            if 100 <= number < 200:\n                assert status.is_informational(number) is True\n            else:\n                assert status.is_informational(number) is False\n\n    def test_status_is_success(self):\n        for number in range(100, 600):\n            if 200 <= number < 300:\n                assert status.is_success(number) is True\n            else:\n                assert status.is_success(number) is False\n\n    def test_status_is_redirect(self):\n        for number in range(100, 600):\n            if 300 <= number < 400:\n                assert status.is_redirect(number) is True\n            else:\n                assert status.is_redirect(number) is False\n\n    def test_status_is_client_error(self):\n        for number in range(100, 600):\n            if 400 <= number < 500:\n                assert status.is_client_error(number) is True\n            else:\n                assert status.is_client_error(number) is False\n\n    def test_status_is_server_error(self):\n        for number in range(100, 600):\n            if 500 <= number < 600:\n                assert status.is_server_error(number) is True\n            else:\n                assert status.is_server_error(number) is False\n\n    def test_statuses_text(self):\n        for _status, number in self.statuses.items():\n            if _status.startswith('HTTP'):\n                assert status.status_text[number] == _status[9:].replace('_', ' ').title()\n            else:  # WS\n                assert status.status_text[number] == _status[8:].replace('_', ' ').title()\n"
  },
  {
    "path": "tests/test_throttling.py",
    "content": "import asyncio\nfrom datetime import datetime, timedelta\nfrom unittest import IsolatedAsyncioTestCase\n\nfrom panther import Panther\nfrom panther.app import API\nfrom panther.configs import config\nfrom panther.test import APIClient\nfrom panther.throttling import Throttle\nfrom panther.utils import round_datetime\n\n\n@API()\nasync def without_throttling_api():\n    return 'ok'\n\n\n@API(throttling=Throttle(rate=3, duration=timedelta(seconds=1)))\nasync def with_throttling_api():\n    return 'ok'\n\n\n@API(throttling=Throttle(rate=1, duration=timedelta(seconds=1)))\nasync def throttling_headers_api():\n    return 'ok'\n\n\nTHROTTLING = Throttle(rate=1, duration=timedelta(seconds=10))\n\nurls = {\n    'without-throttling': without_throttling_api,\n    'with-throttling': with_throttling_api,\n    'throttling-headers': throttling_headers_api,\n}\n\n\nclass TestThrottling(IsolatedAsyncioTestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        app = Panther(__name__, configs=__name__, urls=urls)\n        cls.client = APIClient(app=app)\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n\n    async def test_without_throttling(self):\n        throttling = config.THROTTLING\n        config.THROTTLING = None  # Disable Global Throttling\n\n        res1 = await self.client.get('without-throttling')\n        assert res1.status_code == 200\n\n        res2 = await self.client.get('without-throttling')\n        assert res2.status_code == 200\n\n        res3 = await self.client.get('without-throttling')\n        assert res3.status_code == 200\n        config.THROTTLING = throttling\n\n    async def test_with_throttling(self):\n        res1 = await self.client.get('with-throttling')\n        assert res1.status_code == 200\n\n        res2 = await self.client.get('with-throttling')\n        assert res2.status_code == 200\n\n        res3 = await self.client.get('with-throttling')\n        assert res3.status_code == 200\n\n        res4 = await self.client.get('with-throttling')\n        assert res4.status_code == 429\n\n        res5 = await self.client.get('with-throttling')\n        assert res5.status_code == 429\n\n        await asyncio.sleep(1)  # Sleep and try again\n\n        res6 = await self.client.get('with-throttling')\n        assert res6.status_code == 200\n\n        res7 = await self.client.get('with-throttling')\n        assert res7.status_code == 200\n\n        res8 = await self.client.get('with-throttling')\n        assert res8.status_code == 200\n\n        res9 = await self.client.get('with-throttling')\n        assert res9.status_code == 429\n\n        res10 = await self.client.get('with-throttling')\n        assert res10.status_code == 429\n\n    async def test_throttling_header(self):\n        await self.client.get('throttling-headers')\n\n        res = await self.client.get('throttling-headers')\n        assert res.status_code == 429\n        reset_time = round_datetime(datetime.now(), timedelta(seconds=1)) + timedelta(seconds=1)\n        assert res.headers == {\n            'Content-Type': 'application/json',\n            'Content-Length': '29',\n            'Retry-After': str(int((reset_time - datetime.now()).total_seconds())),\n            'X-RateLimit-Reset': str(int(reset_time.timestamp())),\n        }\n\n    async def test_global_throttling(self):\n        res1 = await self.client.get('without-throttling')\n        assert res1.status_code == 200\n\n        res2 = await self.client.get('without-throttling')\n        assert res2.status_code == 429\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "import os\nfrom datetime import datetime, timedelta\nfrom pathlib import Path\nfrom unittest import TestCase\n\nfrom panther import Panther\nfrom panther.configs import config\nfrom panther.middlewares import HTTPMiddleware\nfrom panther.utils import generate_hash_value_from_string, generate_secret_key, load_env, round_datetime\n\n\nclass TestLoadEnvFile(TestCase):\n    file_path = 'tests/env-test'\n\n    is_active = True\n    db_host = '127.0.0.1'\n    db_port = 27017\n\n    def tearDown(self) -> None:\n        Path(self.file_path).unlink(missing_ok=True)\n\n    def _create_env_file(self, file_data):\n        with open(self.file_path, 'w') as file:\n            file.write(file_data)\n\n    def test_load_env_invalid_file(self):\n        try:\n            load_env('fake.file')\n        except ValueError as e:\n            assert e.args[0] == '\"fake.file\" is not a file.'\n\n    def test_load_env_double_quote(self):\n        self._create_env_file(f\"\"\"\nIS_ACTIVE = \"{self.is_active}\"\nDB_HOST = \"{self.db_host}\"\nDB_PORT = \"{self.db_port}\"\n        \"\"\")\n\n        variables = load_env(self.file_path)\n        assert variables['IS_ACTIVE'] == self.is_active\n        assert variables['DB_HOST'] == self.db_host\n        assert variables['DB_PORT'] == str(self.db_port)\n\n    def test_load_env_single_quote(self):\n        self._create_env_file(f\"\"\"\nIS_ACTIVE = '{self.is_active}'\nDB_HOST = '{self.db_host}'\nDB_PORT = '{self.db_port}'\n                \"\"\")\n\n        variables = load_env(self.file_path)\n        assert variables['IS_ACTIVE'] == self.is_active\n        assert variables['DB_HOST'] == self.db_host\n        assert variables['DB_PORT'] == str(self.db_port)\n\n    def test_load_env_no_quote(self):\n        self._create_env_file(f\"\"\"\nIS_ACTIVE = {self.is_active}\nDB_HOST = {self.db_host}\nDB_PORT = {self.db_port}\n\n                    \"\"\")\n\n        variables = load_env(self.file_path)\n        assert variables['IS_ACTIVE'] == self.is_active\n        assert variables['DB_HOST'] == self.db_host\n        assert variables['DB_PORT'] == str(self.db_port)\n\n    def test_load_env_no_space(self):\n        self._create_env_file(f\"\"\"\nIS_ACTIVE={self.is_active}\nDB_HOST={self.db_host}\nDB_PORT={self.db_port}\n                    \"\"\")\n\n        variables = load_env(self.file_path)\n        assert variables['IS_ACTIVE'] == self.is_active\n        assert variables['DB_HOST'] == self.db_host\n        assert variables['DB_PORT'] == str(self.db_port)\n\n    def test_load_env_not_striped(self):\n        self._create_env_file(f\"\"\"\n        IS_ACTIVE = {self.is_active}\n        DB_HOST = {self.db_host}\n        DB_PORT = {self.db_port}\n                    \"\"\")\n\n        variables = load_env(self.file_path)\n        assert variables['IS_ACTIVE'] == self.is_active\n        assert variables['DB_HOST'] == self.db_host\n        assert variables['DB_PORT'] == str(self.db_port)\n\n    def test_load_env_and_read_from_system_env(self):\n        self._create_env_file(f\"\"\"\nIS_ACTIVE = '{self.is_active}'\nDB_HOST = '{self.db_host}'\nDB_PORT = '{self.db_port}'\n                \"\"\")\n\n        load_env(self.file_path)\n        assert (os.environ['IS_ACTIVE'] == 'True') == self.is_active\n        assert os.environ['DB_HOST'] == self.db_host\n        assert os.environ['DB_PORT'] == str(self.db_port)\n\n\nclass TestUtilFunctions(TestCase):\n    def test_round_datetime_second_1(self):\n        _datetime = datetime(year=1997, month=12, day=25, hour=12, minute=10, second=30)\n        _delta = timedelta(seconds=20)\n        rounded_datetime = round_datetime(_datetime, _delta)\n\n        expected_datetime = datetime(year=1997, month=12, day=25, hour=12, minute=10, second=40)\n        assert rounded_datetime == expected_datetime\n\n    def test_round_datetime_second_2(self):\n        _datetime = datetime(year=1997, month=12, day=25, hour=12, minute=10, second=35)\n        _delta = timedelta(seconds=20)\n        rounded_datetime = round_datetime(_datetime, _delta)\n\n        expected_datetime = datetime(year=1997, month=12, day=25, hour=12, minute=10, second=40)\n        assert rounded_datetime == expected_datetime\n\n    def test_round_datetime_minute_1(self):\n        _datetime = datetime(year=1997, month=12, day=25, hour=12, minute=15, second=30)\n        _delta = timedelta(minutes=20)\n        rounded_datetime = round_datetime(_datetime, _delta)\n\n        expected_datetime = datetime(year=1997, month=12, day=25, hour=12, minute=20)\n        assert rounded_datetime == expected_datetime\n\n    def test_round_datetime_minute_2(self):\n        _datetime = datetime(year=1997, month=12, day=25, hour=12, minute=10, second=30)\n        _delta = timedelta(minutes=20)\n        rounded_datetime = round_datetime(_datetime, _delta)\n\n        expected_datetime = datetime(year=1997, month=12, day=25, hour=12, minute=20)\n        assert rounded_datetime == expected_datetime\n\n    def test_round_datetime_hour_1(self):\n        _datetime = datetime(year=1997, month=12, day=25, hour=12, minute=10, second=30)\n        _delta = timedelta(hours=5)\n        rounded_datetime = round_datetime(_datetime, _delta)\n\n        expected_datetime = datetime(year=1997, month=12, day=25, hour=12)\n        assert rounded_datetime == expected_datetime\n\n    def test_round_datetime_hour_2(self):\n        _datetime = datetime(year=1997, month=12, day=25, hour=10, minute=10, second=30)\n        _delta = timedelta(hours=5)\n        rounded_datetime = round_datetime(_datetime, _delta)\n\n        expected_datetime = datetime(year=1997, month=12, day=25, hour=12)\n        assert rounded_datetime == expected_datetime\n\n    def test_round_datetime_day_1(self):\n        _datetime = datetime(year=1997, month=12, day=25, hour=10, minute=10, second=30)\n        _delta = timedelta(days=5)\n        rounded_datetime = round_datetime(_datetime, _delta)\n\n        expected_datetime = datetime(year=1997, month=12, day=23)\n        assert rounded_datetime == expected_datetime\n\n    def test_round_datetime_day_2(self):\n        _datetime = datetime(year=1997, month=12, day=22, hour=10, minute=10, second=30)\n        _delta = timedelta(days=5)\n        rounded_datetime = round_datetime(_datetime, _delta)\n\n        expected_datetime = datetime(year=1997, month=12, day=23)\n        assert rounded_datetime == expected_datetime\n\n    def test_generate_hash_value_from_string(self):\n        text = 'Hello World'\n        hashed_1 = generate_hash_value_from_string(text)\n        hashed_2 = generate_hash_value_from_string(text)\n\n        assert hashed_1 == hashed_2\n        assert text != hashed_1\n\n\nclass TestLoadConfigs(TestCase):\n    def tearDown(self):\n        config.refresh()\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n\n    def test_urls_not_found(self):\n        global URLs\n        URLs = None\n\n        with self.assertLogs(level='ERROR') as captured:\n            try:\n                Panther(name=__name__, configs=__name__)\n            except SystemExit:\n                assert True\n            else:\n                assert False\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == \"Invalid 'URLs': required.\"\n\n    def test_urls_cant_be_dict(self):\n        global URLs\n        URLs = {}\n\n        with self.assertLogs(level='ERROR') as captured:\n            try:\n                Panther(name=__name__, configs=__name__)\n            except SystemExit:\n                assert True\n            else:\n                assert False\n\n        assert len(captured.records) == 1\n        msg = (\n            \"Invalid 'URLs': can't be 'dict', you may want to pass it's value directly to Panther(). \"\n            'Example: Panther(..., urls=...)'\n        )\n        assert captured.records[0].getMessage() == msg\n\n    def test_urls_not_string(self):\n        global URLs\n        URLs = True\n\n        with self.assertLogs(level='ERROR') as captured:\n            try:\n                Panther(name=__name__, configs=__name__)\n            except SystemExit:\n                assert True\n            else:\n                assert False\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == \"Invalid 'URLs': should be dotted string.\"\n\n    def test_urls_invalid_target(self):\n        global URLs\n        URLs = 'tests.test_utils.TestLoadConfigs'\n\n        with self.assertLogs(level='ERROR') as captured:\n            try:\n                Panther(name=__name__, configs=__name__)\n            except SystemExit:\n                assert True\n            else:\n                assert False\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == \"Invalid 'URLs': should point to a dict.\"\n\n    def test_urls_invalid_module_path(self):\n        global URLs\n        URLs = 'fake.module'\n\n        with self.assertLogs(level='ERROR') as captured:\n            try:\n                Panther(name=__name__, configs=__name__)\n            except SystemExit:\n                assert True\n            else:\n                assert False\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == \"Invalid 'URLs': No module named 'fake'\"\n\n    def test_middlewares_invalid_path(self):\n        global MIDDLEWARES\n        MIDDLEWARES = [('fake.module', {})]\n\n        with self.assertLogs() as captured:\n            try:\n                Panther(name=__name__, configs=__name__, urls={})\n            except SystemExit:\n                assert True\n            else:\n                assert False\n            finally:\n                MIDDLEWARES = []\n\n        assert len(captured.records) == 2\n        assert captured.records[0].levelname == 'WARNING'\n        assert (\n            captured.records[0].getMessage()\n            == \"DEPRECATED 'MIDDLEWARES': `data` does not supported in middlewares anymore, as your data is static you may want to pass them to your middleware with config variables\"\n        )\n        assert captured.records[1].levelname == 'ERROR'\n        assert (\n            captured.records[1].getMessage()\n            == \"Invalid 'MIDDLEWARES': fake.module is not a valid middleware path or type\"\n        )\n\n    def test_middlewares_invalid_structure(self):\n        global MIDDLEWARES\n        MIDDLEWARES = ['fake.module']\n\n        with self.assertLogs(level='ERROR') as captured:\n            try:\n                Panther(name=__name__, configs=__name__, urls={})\n            except SystemExit:\n                assert True\n            else:\n                assert False\n            finally:\n                MIDDLEWARES = []\n\n        assert len(captured.records) == 1\n        assert (\n            captured.records[0].getMessage()\n            == \"Invalid 'MIDDLEWARES': fake.module is not a valid middleware path or type\"\n        )\n\n    def test_middlewares_too_many_args(self):\n        global MIDDLEWARES\n        MIDDLEWARES = [('fake.module', 1, 2)]\n\n        with self.assertLogs(level='ERROR') as captured:\n            try:\n                Panther(name=__name__, configs=__name__, urls={})\n            except SystemExit:\n                assert True\n            else:\n                assert False\n            finally:\n                MIDDLEWARES = []\n\n        assert len(captured.records) == 1\n        assert (\n            captured.records[0].getMessage()\n            == \"Invalid 'MIDDLEWARES': ('fake.module', 1, 2) should be dotted path or type of a middleware class\"\n        )\n\n    def test_middlewares_without_args(self):\n        global MIDDLEWARES\n        MIDDLEWARES = [('tests.test_utils.CorrectTestMiddleware',)]\n\n        with self.assertNoLogs(level='ERROR'):\n            Panther(name=__name__, configs=__name__, urls={})\n\n        MIDDLEWARES = []\n\n    def test_middlewares_invalid_middleware_parent(self):\n        global MIDDLEWARES\n        MIDDLEWARES = [('tests.test_utils.TestMiddleware',)]\n\n        with self.assertLogs(level='ERROR') as captured:\n            try:\n                Panther(name=__name__, configs=__name__, urls={})\n            except SystemExit:\n                assert True\n            else:\n                assert False\n            finally:\n                MIDDLEWARES = []\n\n        assert len(captured.records) == 1\n        assert (\n            captured.records[0].getMessage()\n            == \"Invalid 'MIDDLEWARES': <class 'tests.test_utils.TestMiddleware'> is not a sub class of `HTTPMiddleware`\"\n        )\n\n    def test_jwt_auth_without_secret_key(self):\n        global AUTHENTICATION\n        AUTHENTICATION = 'panther.authentications.JWTAuthentication'\n\n        with self.assertLogs(level='ERROR') as captured:\n            try:\n                Panther(name=__name__, configs=__name__, urls={})\n            except SystemExit:\n                assert True\n            else:\n                assert False\n            finally:\n                AUTHENTICATION = None\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == \"Invalid 'JWTConfig': `JWTConfig.key` or `SECRET_KEY` is required.\"\n\n    def test_jwt_auth_with_secret_key(self):\n        global AUTHENTICATION, SECRET_KEY\n        AUTHENTICATION = 'panther.authentications.JWTAuthentication'\n        SECRET_KEY = generate_secret_key()\n\n        with self.assertNoLogs(level='ERROR'):\n            try:\n                Panther(name=__name__, configs=__name__, urls={})\n            except SystemExit:\n                assert False\n            else:\n                assert True\n            finally:\n                AUTHENTICATION = None\n                SECRET_KEY = None\n\n    def test_check_function_endpoint_decorator(self):\n        with self.assertLogs(level='ERROR') as captured:\n            try:\n                Panther(name=__name__, configs=__name__, urls={'/': invalid_api})\n            except SystemExit:\n                assert True\n            else:\n                assert False\n\n        assert len(captured.records) == 1\n        assert (\n            captured.records[0].getMessage()\n            == 'You may have forgotten to use `@API()` on the `tests.test_utils.invalid_api()`'\n        )\n\n    def test_check_class_endpoint_inheritance(self):\n        with self.assertLogs(level='ERROR') as captured:\n            try:\n                Panther(name=__name__, configs=__name__, urls={'/': InvalidAPI})\n            except SystemExit:\n                assert True\n            else:\n                assert False\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == (\n            'You may have forgotten to inherit from `panther.app.GenericAPI` or `panther.app.GenericWebsocket` '\n            'on the `tests.test_utils.InvalidAPI()`'\n        )\n\n    def test_check_websocket_inheritance(self):\n        with self.assertLogs(level='ERROR') as captured:\n            try:\n                Panther(name=__name__, configs=__name__, urls={'/': InvalidWebsocket})\n            except SystemExit:\n                assert True\n            else:\n                assert False\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == (\n            'You may have forgotten to inherit from `panther.app.GenericAPI` or `panther.app.GenericWebsocket` '\n            'on the `tests.test_utils.InvalidWebsocket()`'\n        )\n\n\ndef invalid_api():\n    pass\n\n\nclass InvalidAPI:\n    pass\n\n\nclass InvalidWebsocket:\n    pass\n\n\nclass CorrectTestMiddleware(HTTPMiddleware):\n    pass\n\n\nclass TestMiddleware:\n    pass\n"
  },
  {
    "path": "tests/test_websockets.py",
    "content": "from unittest import TestCase\n\nimport orjson as json\n\nfrom panther import Panther, status\nfrom panther.authentications import QueryParamJWTAuthentication\nfrom panther.configs import config\nfrom panther.test import WebsocketClient\nfrom panther.websocket import GenericWebsocket\n\n\nclass WithoutAcceptWebsocket(GenericWebsocket):\n    async def connect(self):\n        pass\n\n\nclass CloseOnConnectWebsocket(GenericWebsocket):\n    async def connect(self):\n        await self.accept()\n        await self.close()\n\n\nclass CustomCloseWebsocket(GenericWebsocket):\n    async def connect(self):\n        await self.accept()\n        await self.close(code=status.WS_1013_TRY_AGAIN_LATER, reason='Come Back Later')\n\n\nclass MessageOnConnectWebsocket(GenericWebsocket):\n    async def connect(self):\n        await self.accept()\n        await self.send('Hello')\n        await self.close()\n\n\nclass QueryParamWebsocket(GenericWebsocket):\n    async def connect(self):\n        await self.accept()\n        await self.send(self.query_params)\n        await self.close()\n\n\nclass PathVariableWebsocket(GenericWebsocket):\n    async def connect(self, name: str, age: int, is_male: bool):\n        await self.accept()\n        await self.send(\n            f'{type(name).__name__}({name}), {type(age).__name__}({age}), {type(is_male).__name__}({is_male})',\n        )\n        await self.close()\n\n\nclass SendAllTypesWebsocket(GenericWebsocket):\n    async def connect(self):\n        await self.accept()\n        # String\n        await self.send('Hello Again')\n        # Int\n        await self.send(12)\n        # Dict\n        await self.send({'detail': 'ok'})\n        # List\n        await self.send([1, 2, 3, 'Ali', 4])\n        # Tuple\n        await self.send((1, 2, 3, 'Ali', 4))\n        # Bytes\n        await self.send(b'It Is Value Of A File')\n        # Nothing\n        await self.send()\n        # None\n        await self.send(None)\n        # Close\n        await self.close()\n\n\nclass WebsocketWithoutAuthentication(GenericWebsocket):\n    async def connect(self):\n        await self.accept()\n        await self.send(str(self.user))\n        await self.close()\n\n\nclass WebsocketWithAuthentication(GenericWebsocket):\n    auth = QueryParamJWTAuthentication\n\n    async def connect(self):\n        await self.accept()\n        await self.send(self.user)\n        await self.close()\n\n\nclass Permission:\n    async def __call__(self, connection) -> bool:\n        return connection.path == '/with-permission'\n\n\nclass WebsocketWithPermission(GenericWebsocket):\n    permissions = [Permission]\n\n    async def connect(self):\n        await self.accept()\n        await self.send('ok')\n        await self.close()\n\n\nclass WebsocketWithoutPermission(GenericWebsocket):\n    permissions = [Permission]\n\n    async def connect(self):\n        await self.accept()\n        await self.send('no')\n        await self.close()\n\n\nurls = {\n    'without-accept': WithoutAcceptWebsocket,\n    'close-on-connect': CloseOnConnectWebsocket,\n    'custom-close': CustomCloseWebsocket,\n    'message-after-connect': MessageOnConnectWebsocket,\n    'query-params': QueryParamWebsocket,\n    'path-variable/<name>/<age>/<is_male>/': PathVariableWebsocket,\n    'all-types': SendAllTypesWebsocket,\n    'without-auth': WebsocketWithoutAuthentication,\n    'with-auth': WebsocketWithAuthentication,\n    'with-permission': WebsocketWithPermission,\n    'without-permission': WebsocketWithoutPermission,\n}\n\n\nclass TestWebsocket(TestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        cls.app = Panther(__name__, configs=__name__, urls=urls)\n\n    @classmethod\n    def tearDownClass(cls):\n        config.refresh()\n\n    def test_without_accept(self):\n        ws = WebsocketClient(app=self.app)\n        responses = ws.connect('without-accept')\n        assert responses[0]['type'] == 'websocket.close'\n        assert responses[0]['code'] == 1000\n        assert responses[0]['reason'] == ''\n\n    def test_close_on_connect(self):\n        ws = WebsocketClient(app=self.app)\n        responses = ws.connect('close-on-connect')\n        assert responses[0]['type'] == 'websocket.accept'\n        assert responses[0]['subprotocol'] is None\n        assert responses[0]['headers'] == {}\n\n        assert responses[1]['type'] == 'websocket.close'\n        assert responses[1]['code'] == 1000\n        assert responses[1]['reason'] == ''\n\n    def test_custom_close(self):\n        ws = WebsocketClient(app=self.app)\n        responses = ws.connect('custom-close')\n        assert responses[0]['type'] == 'websocket.accept'\n        assert responses[0]['subprotocol'] is None\n        assert responses[0]['headers'] == {}\n\n        assert responses[1]['type'] == 'websocket.close'\n        assert responses[1]['code'] == 1013\n        assert responses[1]['reason'] == 'Come Back Later'\n\n    def test_query_params(self):\n        ws = WebsocketClient(app=self.app)\n        responses = ws.connect('query-params', query_params={'name': 'ali', 'age': 27})\n        assert responses[0]['type'] == 'websocket.accept'\n        assert responses[0]['subprotocol'] is None\n        assert responses[0]['headers'] == {}\n\n        assert responses[1]['type'] == 'websocket.send'\n        params = json.loads(responses[1]['text'])\n        assert [*params.keys()] == ['name', 'age']\n        assert params['name'] == 'ali'\n        assert params['age'] == '27'\n\n        assert responses[2]['type'] == 'websocket.close'\n        assert responses[2]['code'] == 1000\n        assert responses[2]['reason'] == ''\n\n    def test_message_after_connect(self):\n        ws = WebsocketClient(app=self.app)\n        responses = ws.connect('message-after-connect')\n        assert responses[0]['type'] == 'websocket.accept'\n        assert responses[0]['subprotocol'] is None\n        assert responses[0]['headers'] == {}\n\n        assert responses[1]['type'] == 'websocket.send'\n        assert responses[1]['text'] == 'Hello'\n\n        assert responses[2]['type'] == 'websocket.close'\n        assert responses[2]['code'] == 1000\n        assert responses[2]['reason'] == ''\n\n    def test_path_variables(self):\n        ws = WebsocketClient(app=self.app)\n        responses = ws.connect('path-variable/Ali/25/true')\n        assert responses[0]['type'] == 'websocket.accept'\n        assert responses[0]['subprotocol'] is None\n        assert responses[0]['headers'] == {}\n\n        assert responses[1]['type'] == 'websocket.send'\n        assert responses[1]['text'] == 'str(Ali), int(25), bool(True)'\n\n        assert responses[2]['type'] == 'websocket.close'\n        assert responses[2]['code'] == 1000\n        assert responses[2]['reason'] == ''\n\n    def test_all_types(self):\n        ws = WebsocketClient(app=self.app)\n        responses = ws.connect('all-types')\n\n        assert responses[0]['type'] == 'websocket.accept'\n        assert responses[0]['subprotocol'] is None\n        assert responses[0]['headers'] == {}\n\n        # String\n        assert responses[1]['type'] == 'websocket.send'\n        assert responses[1]['text'] == 'Hello Again'\n        assert 'bytes' not in responses[1]\n        # Int\n        assert responses[2]['type'] == 'websocket.send'\n        assert responses[2]['text'] == '12'\n        assert 'bytes' not in responses[2]\n        # Dict\n        assert responses[3]['type'] == 'websocket.send'\n        assert responses[3]['text'] == '{\"detail\":\"ok\"}'\n        assert 'bytes' not in responses[3]\n        # List\n        assert responses[4]['type'] == 'websocket.send'\n        assert responses[4]['text'] == '[1,2,3,\"Ali\",4]'\n        assert 'bytes' not in responses[4]\n        # Tuple\n        assert responses[5]['type'] == 'websocket.send'\n        assert responses[5]['text'] == '[1,2,3,\"Ali\",4]'\n        assert 'bytes' not in responses[5]\n        # Bytes\n        assert responses[6]['type'] == 'websocket.send'\n        assert responses[6]['bytes'] == b'It Is Value Of A File'\n        assert 'text' not in responses[6]\n        # Nothing\n        assert responses[7]['type'] == 'websocket.send'\n        assert responses[7]['text'] == 'null'\n        assert 'bytes' not in responses[7]\n        # None\n        assert responses[8]['type'] == 'websocket.send'\n        assert responses[8]['text'] == 'null'\n        assert 'bytes' not in responses[8]\n\n        # Close\n        assert responses[9]['type'] == 'websocket.close'\n        assert responses[9]['code'] == 1000\n        assert responses[9]['reason'] == ''\n\n    def test_without_auth(self):\n        ws = WebsocketClient(app=self.app)\n        responses = ws.connect('without-auth')\n        assert responses[0]['type'] == 'websocket.accept'\n        assert responses[0]['subprotocol'] is None\n        assert responses[0]['headers'] == {}\n\n        assert responses[1]['type'] == 'websocket.send'\n        assert responses[1]['text'] == 'None'\n\n        assert responses[2]['type'] == 'websocket.close'\n        assert responses[2]['code'] == 1000\n        assert responses[2]['reason'] == ''\n\n    def test_with_auth_failed(self):\n        ws = WebsocketClient(app=self.app)\n        responses = ws.connect('with-auth')\n\n        assert responses[0]['type'] == 'websocket.accept'\n        assert responses[0]['subprotocol'] is None\n        assert responses[0]['headers'] == {}\n\n        assert responses[1]['type'] == 'websocket.send'\n        assert responses[1]['text'] == 'null'\n\n        assert responses[2]['type'] == 'websocket.close'\n        assert responses[2]['code'] == 1000\n        assert responses[2]['reason'] == ''\n\n    def test_with_auth_success(self):\n        global WS_AUTHENTICATION, SECRET_KEY, DATABASE\n        DATABASE = {\n            'engine': {\n                'class': 'panther.db.connections.PantherDBConnection',\n            },\n        }\n        WS_AUTHENTICATION = 'panther.authentications.QueryParamJWTAuthentication'\n        SECRET_KEY = 'hvdhRspoTPh1cJVBHcuingQeOKNc1uRhIP2k7suLe2g='\n        token = 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.AF3nsj8IQ6t0ncqIx4quoyPfYaZ-pqUOW4z_euUztPM'\n        app = Panther(__name__, configs=__name__, urls=urls)\n\n        ws = WebsocketClient(app=app)\n        with self.assertLogs(level='ERROR') as captured:\n            responses = ws.connect('with-auth', query_params={'authorization': token})\n\n        assert len(captured.records) == 1\n        assert captured.records[0].getMessage() == 'QueryParamJWTAuthentication Error: \"User not found\"'\n\n        assert responses[0]['type'] == 'websocket.close'\n        assert responses[0]['code'] == 1000\n        assert responses[0]['reason'] == ''\n\n    def test_with_permission(self):\n        ws = WebsocketClient(app=self.app)\n        responses = ws.connect('with-permission')\n\n        assert responses[0]['type'] == 'websocket.accept'\n        assert responses[0]['subprotocol'] is None\n        assert responses[0]['headers'] == {}\n\n        assert responses[1]['type'] == 'websocket.send'\n        assert responses[1]['text'] == 'ok'\n\n        assert responses[2]['type'] == 'websocket.close'\n        assert responses[2]['code'] == 1000\n        assert responses[2]['reason'] == ''\n\n    def test_without_permission(self):\n        ws = WebsocketClient(app=self.app)\n        responses = ws.connect('without-permission')\n\n        assert responses[0]['type'] == 'websocket.close'\n        assert responses[0]['code'] == 1000\n        assert responses[0]['reason'] == ''\n"
  }
]