├── .github └── workflows │ ├── cli_test.yaml │ ├── deploy_docs.yaml │ ├── integration_test.yaml │ ├── release.yaml │ ├── release_beta.yaml │ └── tests.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── async_orm.md ├── blank.md ├── book_resource_api_docs.png ├── cli.md ├── controllers.md ├── dependency_injection.md ├── docker.md ├── getting_started.md ├── guards.md ├── imgs │ ├── DI.png │ ├── module-architecture.png │ ├── pynest-logo.png │ ├── pynest_new_logo_opt-removebg-preview.jpeg │ └── pynest_new_logo_opt.png ├── index.html ├── introduction.md ├── license.md ├── lifespan_tasks.md ├── modules.md ├── mongodb.md ├── providers.md ├── styles │ └── extra.css └── sync_orm.md ├── examples ├── BlankApp │ ├── __init__.py │ ├── main.py │ └── src │ │ ├── __init__.py │ │ ├── app_controller.py │ │ ├── app_module.py │ │ ├── app_service.py │ │ ├── example │ │ ├── __init__.py │ │ ├── example_controller.py │ │ ├── example_model.py │ │ ├── example_module.py │ │ └── example_service.py │ │ ├── product │ │ ├── __init__.py │ │ ├── product_controller.py │ │ ├── product_model.py │ │ ├── product_module.py │ │ └── product_service.py │ │ └── user │ │ ├── __init__.py │ │ ├── user_controller.py │ │ ├── user_model.py │ │ ├── user_module.py │ │ └── user_service.py ├── CommandLineApp │ ├── __init__.py │ ├── main.py │ └── src │ │ ├── __init__.py │ │ ├── app_controller.py │ │ ├── app_module.py │ │ ├── app_service.py │ │ └── user │ │ ├── __init__.py │ │ ├── user_controller.py │ │ ├── user_module.py │ │ └── user_service.py ├── MongoApp │ ├── __init__.py │ ├── main.py │ └── src │ │ ├── __init__.py │ │ ├── app_controller.py │ │ ├── app_module.py │ │ ├── app_service.py │ │ ├── config.py │ │ ├── example │ │ ├── __init__.py │ │ ├── example_controller.py │ │ ├── example_entity.py │ │ ├── example_model.py │ │ ├── example_module.py │ │ └── example_service.py │ │ ├── product │ │ ├── __init__.py │ │ ├── product_controller.py │ │ ├── product_entity.py │ │ ├── product_model.py │ │ ├── product_module.py │ │ └── product_service.py │ │ └── user │ │ ├── __init__.py │ │ ├── user_controller.py │ │ ├── user_entity.py │ │ ├── user_model.py │ │ ├── user_module.py │ │ └── user_service.py ├── OrmAsyncApp │ ├── __init__.py │ ├── main.py │ └── src │ │ ├── __init__.py │ │ ├── app_controller.py │ │ ├── app_module.py │ │ ├── app_service.py │ │ ├── config.py │ │ ├── example │ │ ├── __init__.py │ │ ├── example_controller.py │ │ ├── example_entity.py │ │ ├── example_model.py │ │ ├── example_module.py │ │ └── example_service.py │ │ ├── product │ │ ├── __init__.py │ │ ├── product_controller.py │ │ ├── product_entity.py │ │ ├── product_model.py │ │ ├── product_module.py │ │ └── product_service.py │ │ └── user │ │ ├── __init__.py │ │ ├── user_controller.py │ │ ├── user_entity.py │ │ ├── user_model.py │ │ ├── user_module.py │ │ └── user_service.py ├── OrmSyncApp │ ├── __init__.py │ ├── main.py │ └── src │ │ ├── __init__.py │ │ ├── app_controller.py │ │ ├── app_module.py │ │ ├── app_service.py │ │ ├── config.py │ │ ├── example │ │ ├── __init__.py │ │ ├── example_controller.py │ │ ├── example_entity.py │ │ ├── example_model.py │ │ ├── example_module.py │ │ └── example_service.py │ │ ├── product │ │ ├── __init__.py │ │ ├── product_controller.py │ │ ├── product_entity.py │ │ ├── product_model.py │ │ ├── product_module.py │ │ └── product_service.py │ │ └── user │ │ ├── __init__.py │ │ ├── user_controller.py │ │ ├── user_entity.py │ │ ├── user_model.py │ │ ├── user_module.py │ │ └── user_service.py ├── __init__.py └── guard_examples.py ├── mkdocs.yml ├── nest ├── __init__.py ├── cli │ ├── __init__.py │ ├── cli.py │ ├── click_handlers.py │ ├── src │ │ ├── __init__.py │ │ ├── app_controller.py │ │ ├── app_module.py │ │ ├── app_service.py │ │ └── generate │ │ │ ├── __init__.py │ │ │ ├── generate_controller.py │ │ │ ├── generate_model.py │ │ │ ├── generate_module.py │ │ │ └── generate_service.py │ └── templates │ │ ├── __init__.py │ │ ├── abstract_empty_template.py │ │ ├── base_template.py │ │ ├── blank_template.py │ │ ├── cli_templates.py │ │ ├── mongo_db_template.py │ │ ├── mongo_template.py │ │ ├── mysql_template.py │ │ ├── orm_template.py │ │ ├── postgres_template.py │ │ ├── relational_db_template.py │ │ ├── sqlite_template.py │ │ └── templates_factory.py ├── common │ ├── __init__.py │ ├── constants.py │ ├── exceptions.py │ ├── module.py │ └── route_resolver.py └── core │ ├── __init__.py │ ├── cli_factory.py │ ├── database │ ├── __init__.py │ ├── base_config.py │ ├── odm_config.py │ ├── odm_provider.py │ ├── orm_config.py │ └── orm_provider.py │ ├── decorators │ ├── __init__.py │ ├── class_based_view.py │ ├── cli │ │ ├── __init__.py │ │ └── cli_decorators.py │ ├── controller.py │ ├── database.py │ ├── guards.py │ ├── http_code.py │ ├── http_method.py │ ├── injectable.py │ ├── module.py │ └── utils.py │ ├── pynest_app_context.py │ ├── pynest_application.py │ ├── pynest_container.py │ └── pynest_factory.py ├── pyproject.toml └── tests ├── __init__.py ├── test_common ├── __init__.py ├── test_module.py └── test_route_resolver.py └── test_core ├── __init__.py ├── test_app_context.py ├── test_database ├── __init__.py ├── test_odm.py └── test_orm.py ├── test_decorators ├── __init__.py ├── test_controller.py └── test_guard.py ├── test_pynest_application.py ├── test_pynest_container.py └── test_pynest_factory.py /.github/workflows/cli_test.yaml: -------------------------------------------------------------------------------- 1 | name: CLI Test 2 | 3 | on: [push, workflow_call, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | app_type: ["Blank", "SyncORM", "AsyncORM", "MongoDB", "PostgresSync", "PostgresAsync", "MySQLSync", "MySQLAsync"] 12 | 13 | steps: 14 | - name: Check out repository code 15 | uses: actions/checkout@v3 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.9' 21 | 22 | - name: Install dependencies 23 | run: | 24 | pip install poetry 25 | poetry install 26 | 27 | - name: Test CLI Commands 28 | run: | 29 | app_name="${{ matrix.app_type }}App" 30 | case "${{ matrix.app_type }}" in 31 | "Blank") 32 | poetry run pynest generate application -n "$app_name" 33 | ;; 34 | "SyncORM") 35 | poetry run pynest generate application -n "$app_name" -db sqlite 36 | ;; 37 | "AsyncORM") 38 | poetry run pynest generate application -n "$app_name" -db sqlite --is-async 39 | ;; 40 | "MongoDB") 41 | poetry run pynest generate application -n "$app_name" -db mongodb 42 | ;; 43 | "PostgresSync") 44 | poetry run pynest generate application -n "$app_name" -db postgresql 45 | ;; 46 | "PostgresAsync") 47 | poetry run pynest generate application -n "$app_name" -db postgresql --is-async 48 | ;; 49 | "MySQLSync") 50 | poetry run pynest generate application -n "$app_name" -db mysql 51 | ;; 52 | "MySQLAsync") 53 | poetry run pynest generate application -n "$app_name" -db mysql --is-async 54 | ;; 55 | esac 56 | 57 | cd "$app_name" 58 | poetry run pynest generate resource -n user 59 | 60 | - name: Verify Boilerplate 61 | run: | 62 | app_name="${{ matrix.app_type }}App" 63 | if [ -d "$app_name" ]; then 64 | echo "Directory $app_name exists." 65 | else 66 | echo "Directory $app_name does not exist." 67 | exit 1 68 | fi 69 | 70 | if [ -d "$app_name/src/user" ]; then 71 | echo "Directory $app_name/src/user exists." 72 | else 73 | echo "Directory $app_name does not exist." 74 | exit 1 75 | fi 76 | 77 | # List of expected files 78 | declare -a files=("main.py" "requirements.txt" "README.md") 79 | declare -a src_level_files=("app_module.py" "app_service.py" "app_controller.py") 80 | declare -a module_files=("user_controller.py" "user_service.py" "user_module.py" "user_model.py") 81 | 82 | # Check each file in the list of files 83 | for file in "${files[@]}"; do 84 | if [ -f "$app_name/$file" ]; then 85 | echo "$file exists in $app_name." 86 | else 87 | echo "$file does not exist in $app_name." 88 | exit 1 89 | fi 90 | done 91 | 92 | # Check each file in the list of files 93 | for file in "${src_level_files[@]}"; do 94 | if [ -f "$app_name/src/$file" ]; then 95 | echo "$file exists in $app_name." 96 | else 97 | echo "$file does not exist in $app_name." 98 | exit 1 99 | fi 100 | done 101 | 102 | # Check each file in the list of module_files 103 | for file in "${module_files[@]}"; do 104 | if [ -f "$app_name/src/user/$file" ]; then 105 | echo "$file exists in $app_name." 106 | else 107 | echo "$file does not exist in $app_name." 108 | exit 1 109 | fi 110 | done 111 | 112 | echo "Boilerplate for ${{ matrix.app_type }} generated successfully." -------------------------------------------------------------------------------- /.github/workflows/deploy_docs.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | 3 | on: [workflow_call, workflow_dispatch] 4 | 5 | permissions: 6 | contents: read 7 | pages: write 8 | id-token: write 9 | 10 | concurrency: 11 | group: "pages" 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | deploy_docs: 16 | environment: 17 | name: github-pages 18 | url: ${{ steps.deployment.outputs.page_url }} 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | 25 | - name: Copy License File 26 | run: cp LICENSE docs/license.md 27 | 28 | - name: Set up Python 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: "3.9" 32 | 33 | - name: Install dependencies 34 | run: | 35 | pip install poetry 36 | poetry install --with docs 37 | 38 | - name: Build docs 39 | run: poetry run mkdocs build --clean 40 | 41 | - name: Setup Pages 42 | uses: actions/configure-pages@v3 43 | 44 | - name: Upload artifact 45 | uses: actions/upload-pages-artifact@v1 46 | with: 47 | path: 'site' 48 | 49 | - name: Deploy to GitHub Pages 50 | id: deployment 51 | uses: actions/deploy-pages@v1 52 | -------------------------------------------------------------------------------- /.github/workflows/integration_test.yaml: -------------------------------------------------------------------------------- 1 | name: Integration Test 2 | 3 | on: [push, workflow_call, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | app_type: ["Blank", "SyncORM", "AsyncORM"] 12 | 13 | steps: 14 | - name: Check out repository code 15 | uses: actions/checkout@v3 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.9' 21 | 22 | - name: Install dependencies 23 | run: | 24 | pip install poetry 25 | poetry install 26 | 27 | - name: Start Application 28 | run: | 29 | if [ "${{ matrix.app_type }}" == "Blank" ]; then 30 | app_name="NestApp" 31 | is_async="" 32 | elif [ "${{ matrix.app_type }}" == "SyncORM" ]; then 33 | app_name="ORMNestApp" 34 | is_async="" 35 | elif [ "${{ matrix.app_type }}" == "AsyncORM" ]; then 36 | app_name="AsyncORMNestApp" 37 | is_async="--is-async" 38 | fi 39 | 40 | if [ "${{ matrix.app_type }}" == "Blank" ]; then 41 | poetry run pynest generate application -n "$app_name" 42 | else 43 | poetry run pynest generate application -n "$app_name" -db sqlite $is_async 44 | poetry add aiosqlite 45 | fi 46 | 47 | cd "$app_name" 48 | poetry run pynest generate resource -n user 49 | poetry run uvicorn "src.app_module:http_server" --host "0.0.0.0" --port 8000 --reload & 50 | 51 | - name: Wait for the server to start 52 | run: sleep 10 53 | 54 | - name: Test the application 55 | run: | 56 | curl -f http://localhost:8000/docs 57 | curl -f -X 'POST' \ 58 | "http://localhost:8000/user/" \ 59 | -H 'accept: application/json' \ 60 | -H 'Content-Type: application/json' \ 61 | -d '{"name": "Example Name"}' 62 | curl -f http://localhost:8000/user/ 63 | 64 | - name: Kill the server 65 | run: kill $(jobs -p) || true -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release Package 2 | 3 | on: 4 | workflow_dispatch: 5 | permissions: 6 | users: 7 | - ItayTheDar 8 | inputs: 9 | increment_version: 10 | description: 'Increment version by major, minor, or patch' 11 | required: true 12 | default: 'patch' 13 | type: choice 14 | options: 15 | - major 16 | - minor 17 | - patch 18 | 19 | env: 20 | VERSION_FILE_PATH: pyproject.toml 21 | CHANGELOG_FILE_PATH: CHANGELOG.md 22 | 23 | jobs: 24 | run-tests: 25 | uses: ./.github/workflows/tests.yaml 26 | 27 | release-package: 28 | runs-on: ubuntu-latest 29 | needs: run-tests 30 | permissions: 31 | contents: write 32 | 33 | steps: 34 | - name: Checkout repository 35 | uses: actions/checkout@v3 36 | with: 37 | token: ${{ secrets.RELEASE_GIT_TOKEN }} 38 | 39 | - name: Install Python 40 | uses: actions/setup-python@v4 41 | with: 42 | python-version: "3.10" 43 | 44 | - name: Install Poetry 45 | run: | 46 | pip install poetry 47 | 48 | - name: Set version using Poetry 49 | run: | 50 | # Extract the current version 51 | CURRENT_VERSION=$(poetry version -s) 52 | echo "Current version: $CURRENT_VERSION" 53 | 54 | # Increment version 55 | case ${{ inputs.increment_version }} in 56 | major) 57 | NEW_VERSION=$(poetry version major | awk '{print $NF}') 58 | ;; 59 | minor) 60 | NEW_VERSION=$(poetry version minor | awk '{print $NF}') 61 | ;; 62 | patch) 63 | NEW_VERSION=$(poetry version patch | awk '{print $NF}') 64 | ;; 65 | *) 66 | echo "Invalid input for increment_version" 67 | exit 1 68 | ;; 69 | esac 70 | echo "New version: $NEW_VERSION" 71 | echo "RELEASE_VERSION=$NEW_VERSION" >> $GITHUB_ENV 72 | 73 | - name: Install build dependencies 74 | run: | 75 | poetry install --with build 76 | 77 | - name: Update CHANGELOG.md 78 | run: poetry run git-changelog . -o $CHANGELOG_FILE_PATH 79 | 80 | - name: Build package with Poetry 81 | run: | 82 | poetry build 83 | 84 | - name: Commit and push changes 85 | run: | 86 | git config --global user.name "github-actions" 87 | git config --global user.email "github@actions.com" 88 | git add $CHANGELOG_FILE_PATH pyproject.toml 89 | git commit -m "Increment version to $NEW_VERSION" 90 | git push 91 | 92 | - name: Publish package to PyPI 93 | run: | 94 | poetry publish --username ${{ secrets.PYPI_API_USER }} --password ${{ secrets.PYPI_API_TOKEN }} 95 | 96 | - name: Create GitHub release 97 | uses: softprops/action-gh-release@v1 98 | with: 99 | name: v${{ env.RELEASE_VERSION }} 100 | tag_name: v${{ env.RELEASE_VERSION }} 101 | 102 | deploy-docs: 103 | needs: release-package 104 | permissions: 105 | contents: read 106 | pages: write 107 | id-token: write 108 | uses: ./.github/workflows/deploy_docs.yaml 109 | -------------------------------------------------------------------------------- /.github/workflows/release_beta.yaml: -------------------------------------------------------------------------------- 1 | name: Release Beta 2 | 3 | on: 4 | workflow_dispatch: 5 | permissions: 6 | users: 7 | - ItayTheDar 8 | inputs: 9 | increment_version: 10 | description: 'Increment version by major, minor, or patch' 11 | required: true 12 | default: 'patch' 13 | type: choice 14 | options: 15 | - major 16 | - minor 17 | - patch 18 | 19 | 20 | env: 21 | VERSION_FILE_PATH: nest/__init__.py 22 | CHANGELOG_FILE_PATH: CHANGELOG.md 23 | 24 | jobs: 25 | run-tests: 26 | uses: ./.github/workflows/tests.yaml 27 | 28 | release-beta: 29 | runs-on: ubuntu-latest 30 | needs: run-tests 31 | permissions: 32 | contents: write 33 | 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v3 37 | with: 38 | token: ${{ secrets.RELEASE_GIT_TOKEN }} 39 | 40 | - name: Set version 41 | run: | 42 | VERSION_REGEX='^(__version__ = \")(([[:digit:]]+\.)*[[:digit:]]+)((a|b|rc)[[:digit:]]+)?(\.post[[:digit:]]+)?(.dev[[:digit:]]+)?(\")$' 43 | 44 | # get current version from version file 45 | CURRENT_VERSION=`sed -n -E "s/$VERSION_REGEX/\2/p" $VERSION_FILE_PATH` 46 | echo "Current version: $CURRENT_VERSION" 47 | 48 | # switch case for incrementing_version based on input 49 | case ${{ inputs.increment_version }} in 50 | major) 51 | NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{$1=$1+1; $2=0; $3=0; print $0}' OFS=".") 52 | ;; 53 | minor) 54 | NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{$2=$2+1; $3=0; print $0}' OFS=".") 55 | ;; 56 | patch) 57 | NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{$3=$3+1; print $0}' OFS=".") 58 | ;; 59 | *) 60 | echo "Invalid input for increment_version" 61 | exit 1 62 | ;; 63 | esac 64 | echo "New version: $NEW_VERSION" 65 | echo "RELEASE_VERSION=$NEW_VERSION" >> $GITHUB_ENV 66 | 67 | # update version file 68 | sed -i -E "s/$VERSION_REGEX/\1$NEW_VERSION\8/" $VERSION_FILE_PATH 69 | 70 | 71 | - name: Install python 72 | uses: actions/setup-python@v4 73 | with: 74 | python-version: "3.10" 75 | 76 | - name: Install python dependencies 77 | run: | 78 | pip install --upgrade pip 79 | pip install -r requirements-release.txt 80 | 81 | - name: Update CHANGELOG.md 82 | run: git-changelog . -o $CHANGELOG_FILE_PATH 83 | 84 | 85 | - name: Build package 86 | run: python -m build 87 | 88 | - name: Publish package to TestPyPI 89 | run: | 90 | python -m twine upload dist/* -r testpypi -u ${{ secrets.TEST_PYPI_API_USER }} -p ${{ secrets.TEST_PYPI_API_TOKEN }} 91 | 92 | - name: Install test package 93 | run: | 94 | pip install --index-url https://test.pypi.org/simple/ --no-deps pynest-api 95 | 96 | # import to check if package is installed correctly 97 | python -c "import nest" 98 | 99 | - name: Copy pip url 100 | run: | 101 | echo "pip install --index-url https://test.pypi.org/simple/ --no-deps pynest-api" -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, workflow_call, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.9", "3.10", "3.11"] 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Install Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | 20 | - name: Install dependencies 21 | run: | 22 | pip install poetry 23 | poetry install --with test 24 | 25 | - name: Run tests 26 | run: | 27 | poetry run pytest tests -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-specific 2 | __pycache__/ 3 | *.pyc 4 | *.pyo 5 | *.pyd 6 | 7 | # Dependency directories 8 | venv/ 9 | env/ 10 | envs/ 11 | .env 12 | poetry.lock 13 | requirements.txt 14 | 15 | # Compiled source 16 | *.com 17 | *.class 18 | *.dll 19 | *.exe 20 | *.o 21 | *.so 22 | 23 | # Logs and databases 24 | *.log 25 | *.sqlite 26 | *.db 27 | 28 | # Development and IDE files 29 | .vscode/ 30 | .idea/ 31 | *.swp 32 | *~ 33 | 34 | # Compiled files 35 | dist/ 36 | build/ 37 | *.egg-info/ 38 | 39 | # Testing 40 | htmlcov/ 41 | .coverage 42 | .tox/ 43 | 44 | # Miscellaneous 45 | .DS_Store 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | 9 | ## Unreleased 10 | 11 | [Compare with latest](https://github.com/PythonNest/PyNest/compare/44cfa3388619453b0fac619802060ebafb3f4074...HEAD) 12 | 13 | ### Added 14 | 15 | - Add guard support (#107) ([44cfa33](https://github.com/PythonNest/PyNest/commit/44cfa3388619453b0fac619802060ebafb3f4074) by Itay Dar). 16 | 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 PyNest 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docs/blank.md: -------------------------------------------------------------------------------- 1 | # Blank Application with PyNest 2 | 3 | ## Introduction 4 | 5 | This documentation introduces a creation of the simplest Pynest Application. 6 | 7 | ### Requirements 8 | 9 | - Python 3.9+ 10 | - PyNest (latest version) 11 | 12 | ## Setting Up 13 | 14 | ### Installation and Setup 15 | 16 | Ensure you have the latest version of PyNest installed. You can install them using pip: 17 | 18 | ```bash 19 | pip install pynest-api 20 | ``` 21 | 22 | ## Start with cli 23 | 24 | #### Create a new project 25 | 26 | ```bash 27 | pynest create-nest-app -n my_app_name 28 | ``` 29 | 30 | this command will create a new project with the following structure: 31 | 32 | ```text 33 | ├── app.py 34 | ├── main.py 35 | |── requirements.txt 36 | |── README.md 37 | ├── src 38 | │ ├── __init__.py 39 | │ ├── app_module.py 40 | ├── |── app_controller.py 41 | ├── |── app_service.py 42 | ``` 43 | 44 | After creating the project, let's create a new module: 45 | 46 | ```bash 47 | pynest g module -n examples 48 | ``` 49 | 50 | This will create a new module called examples in your application with the following structure under the src folder: 51 | 52 | ```text 53 | ├── examples 54 | │ ├── __init__.py 55 | │ ├── examples_controller.py 56 | │ ├── examples_service.py 57 | │ ├── examples_model.py 58 | │ ├── examples_module.py 59 | ``` 60 | 61 | Let's go over the boilerplate code the cli generated: 62 | 63 | `app_service.py` 64 | ```python 65 | from nest.core import Injectable 66 | 67 | 68 | @Injectable 69 | class AppService: 70 | def __init__(self): 71 | self.app_name = "BlankApp" 72 | self.app_version = "1.0.0" 73 | 74 | async def get_app_info(self): 75 | return {"app_name": self.app_name, "app_version": self.app_version} 76 | ``` 77 | 78 | `app_controller.py` 79 | ```python 80 | from nest.core import Controller, Get 81 | 82 | from .app_service import AppService 83 | 84 | 85 | @Controller("/") 86 | class AppController: 87 | 88 | def __init__(self, service: AppService): 89 | self.service = service 90 | 91 | @Get("/") 92 | async def get_app_info(self): 93 | return await self.service.get_app_info() 94 | ``` 95 | 96 | `app_module.py` 97 | 98 | ```python 99 | from nest.core import PyNestFactory, Module 100 | from src.example.example_module import ExampleModule 101 | from .app_controller import AppController 102 | from .app_service import AppService 103 | from fastapi import FastAPI 104 | 105 | 106 | @Module( 107 | controllers=[AppController], 108 | providers=[AppService], 109 | imports=[ExampleModule], 110 | ) 111 | class AppModule: 112 | pass 113 | 114 | 115 | app = PyNestFactory.create(AppModule, description="This is my FastAPI app", title="My App", version="1.0.0", debug=True) 116 | 117 | http_server: FastAPI = app.get_server() 118 | ``` 119 | 120 | `@Module(...)`: This is a decorator that defines a module. In PyNest, a module is a class annotated with a `@Module()` decorator. 121 | The imports array includes the modules required by this module. In this case, ExampleModule is imported. The controllers and providers arrays are empty here, indicating this module doesn't directly provide any controllers or services. 122 | 123 | `PyNestFactory.create()` is a command to create an instance of the application. 124 | The AppModule is passed as an argument, which acts as the root module of the application. 125 | Additional metadata like description, title, version, and debug flag are also provided 126 | 127 | `http_server: FastAPI = app.get_server()`: Retrieves the HTTP server instance from the application. 128 | 129 | ### Creating Model 130 | 131 | Create a model to represent the data that will be stored in the database. 132 | 133 | `examples_model.py` 134 | 135 | ```python 136 | from pydantic import BaseModel 137 | 138 | 139 | class Examples(BaseModel): 140 | name: str 141 | ``` 142 | 143 | ### Creating Service 144 | 145 | Implement services to handle business logic. 146 | 147 | `examples_service.py` 148 | 149 | ```python 150 | from .examples_model import Examples 151 | from nest.core import Injectable 152 | 153 | 154 | @Injectable 155 | class ExamplesService: 156 | def __init__(self): 157 | self.database = [] 158 | 159 | async def get_examples(self): 160 | return self.database 161 | 162 | async def add_examples(self, examples: Examples): 163 | self.database.append(examples) 164 | return {"message": "Example added successfully"} 165 | 166 | ``` 167 | 168 | create a controller to handle the requests and responses. The controller should call the service to execute business 169 | logic. 170 | 171 | `example_controller.py` 172 | 173 | ```python 174 | from nest.core import Controller, Get, Post 175 | 176 | from .examples_service import ExamplesService 177 | from .examples_model import Examples 178 | 179 | 180 | @Controller("examples") 181 | class ExamplesController: 182 | 183 | def __init__(self, service: ExamplesService): 184 | self.service = service 185 | 186 | @Get("/") 187 | async def get_examples(self): 188 | return await self.service.get_examples() 189 | 190 | @Post("/") 191 | async def add_examples(self, examples: Examples): 192 | return await self.service.add_examples(examples) 193 | ``` 194 | 195 | ## Creating Module 196 | 197 | create the module file to register the controller and the service 198 | 199 | `examples_module.py` 200 | 201 | ```python 202 | from nest.core import Module 203 | from .examples_controller import ExamplesController 204 | from .examples_service import ExamplesService 205 | 206 | 207 | @Module( 208 | controllers=[ExamplesController], 209 | providers=[ExamplesService], 210 | ) 211 | class ExamplesModule: 212 | pass 213 | ``` 214 | 215 | ## Run the application 216 | 217 | ```shell 218 | uvicorn "src.app_module:http_server" --host "0.0.0.0" --port "8000" --reload 219 | ``` 220 | 221 | Now you can access the application at http://localhost:8000/docs and test the endpoints. 222 | 223 | --- 224 | 225 | 233 | -------------------------------------------------------------------------------- /docs/book_resource_api_docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/docs/book_resource_api_docs.png -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | # PyNest CLI Deep Dive 🔍 2 | 3 | The PyNest CLI is a powerful tool that helps you quickly scaffold, develop, and manage your PyNest applications. This guide will walk you through all the commands and best practices to make the most out of the PyNest CLI. 4 | 5 | ## Installation 💻 6 | 7 | Before using the PyNest CLI, ensure you have it installed. You can install it using pip: 8 | 9 | ```bash 10 | pip install pynest-api 11 | ``` 12 | 13 | ## CLI Commands and Usage 🛠️ 14 | The PyNest CLI provides a variety of commands to help you create and manage your PyNest applications. Below are the detailed descriptions of the available commands and their usage. 15 | 16 | ### Generate New Application 17 | Create a new PyNest application. 18 | 19 | ```bash 20 | pynest generate application --app-name [--db-type ] [--is-async] 21 | ``` 22 | **Options** 23 | 24 | * `--app-name`, `-n`: The name of the new application. (Required) 25 | * `--db-type`, `-db`: The type of database to use (postgresql, mysql, sqlite, mongodb). (Optional) 26 | * `--is-async`: Whether the project should be async or not. This is applicable only for relational databases. (Optional) 27 | * `--is-cli`: Whether the project should be a CLI App. (Optional) 28 | 29 | #### Example 30 | ```bash 31 | pynest generate application --app-name my_pynest_app --db-type postgresql --is-async 32 | ``` 33 | 34 | This example will create a skeleton PyNest application named `my_pynest_app` with PostgreSQL as the database and async support. 35 | 36 | #### File Structure 🗂️ 37 | Here's the typical file structure of a PyNest application generated by the CLI: 38 | 39 | ```text 40 | my_pynest_app/ 41 | ├── src/ 42 | │ ├── __init__.py 43 | │ ├── app_module.py 44 | │ ├── app_controller.py 45 | │ ├── app_service.py 46 | │ ├── config.py 47 | ├── main.py 48 | ├── requirements.txt 49 | └── README.md 50 | ``` 51 | 52 | !Note: The actual file structure may vary based on the modules and components you create 53 | 54 | 55 | ### generate command 56 | Generate boilerplate code for various components of the application. 57 | 58 | #### Subcommands 59 | 60 | **Resource** 61 | Generate a new resource with the associated controller, service, model and module files, with respect to the project configurations (e.g. database type). 62 | 63 | ```bash 64 | pynest generate resource --name 65 | ``` 66 | 67 | **Options** 68 | 69 | 70 | * `--name`, `-n`: The name of the new module. (Required) 71 | * `--path`, `-p`: The path where the module should be created. (Optional) 72 | 73 | **Example** 74 | ```bash 75 | pynest generate resource --name users 76 | ``` 77 | 78 | This will create a new resource named `users` with the associated controller, service, model and module files. 79 | 80 | 81 | **Module** 82 | 83 | Generate a new module file, which can be used to group related components of the application. 84 | 85 | ```bash 86 | pynest generate module --name 87 | ``` 88 | 89 | **Options** 90 | 91 | * `--name`, `-n`: The name of the new module. (Required) 92 | * `--path`, `-p`: The path where the module should be created. (Optional) 93 | 94 | **Example** 95 | ```bash 96 | pynest generate module --name auth 97 | ``` 98 | 99 | This will create a new module named `auth` in the default path. 100 | 101 | **Controller** 102 | 103 | Generate a new controller file, which will handle the routing and responses for a specific resource. 104 | 105 | ```bash 106 | pynest generate controller --name 107 | ``` 108 | 109 | **Options** 110 | 111 | * `--name`, `-n`: The name of the new controller. (Required) 112 | * `--path`, `-p`: The path where the controller should be created. (Optional) 113 | 114 | **Example** 115 | ```bash 116 | pynest generate controller --name users 117 | ``` 118 | 119 | This will create a new controller named `users` in the default path. 120 | 121 | **Service** 122 | 123 | Generate a new service file, which will contain the business logic for a specific resource. 124 | 125 | ```bash 126 | pynest generate service --name 127 | ``` 128 | 129 | **Options** 130 | 131 | * `--name`, `-n`: The name of the new service. (Required) 132 | * `--path`, `-p`: The path where the service should be created. (Optional) 133 | 134 | **Example** 135 | ```bash 136 | pynest generate service --name users 137 | ``` 138 | 139 | This will create a new service named `users` in the default path. 140 | 141 | 142 | ## Best Practices 🌟 143 | 144 | To ensure a clean and maintainable codebase, follow these best practices when using the PyNest CLI: 145 | 146 | * **Consistent Naming Conventions**: Use consistent naming conventions for files and directories. For example, use lowercase with underscores for module names (users_module.py) and camelCase for class names (UsersController)._ 147 | 148 | * **Modular Structure**: Keep your code modular. Each module should encapsulate a specific functionality of your application. 149 | 150 | * **Service Layer**: Keep business logic within services to maintain separation of concerns. Controllers should handle HTTP requests and responses only. 151 | 152 | * **Dependency Injection**: Use dependency injection to manage dependencies between services and controllers. This makes your code more testable and maintainable. 153 | 154 | * **Environment Configuration**: Use environment variables to manage configuration settings, such as database connection strings and API keys. 155 | 156 | The PyNest CLI is a powerful tool that simplifies the development of PyNest applications. By following the best practices and utilizing the commands effectively, you can build scalable and maintainable applications with ease. Happy coding! 157 | 158 | --- 159 | -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This guide will help you get started with setting up a new PyNest project, creating the essential modules, and running your application. 4 | 5 | ## Installation 6 | 7 | To install PyNest, ensure you have Python 3.9+ installed. Then, install PyNest using pip: 8 | 9 | ```bash 10 | pip install pynest-api 11 | ``` 12 | 13 | ## Creating a New Project 📂 14 | 15 | Start by creating a new directory for your project and navigating into it: 16 | 17 | ```bash 18 | mkdir my_pynest_project 19 | cd my_pynest_project 20 | ``` 21 | 22 | ## Creating the Application Structure 🏗️ 23 | 24 | We'll create the basic structure of a PyNest application. Here's what it will look like: 25 | 26 | ```text 27 | my_pynest_project/ 28 | ├── src/ 29 | │ ├── __init__.py 30 | │ ├── app_module.py 31 | │ ├── app_controller.py 32 | │ ├── app_service.py 33 | ├── main.py 34 | ├── requirements.txt 35 | └── README.md 36 | ``` 37 | 38 | ### Step 1: Create app_module.py 39 | 40 | The app_module.py file is where we define our main application module. Create the file and add the following code: 41 | 42 | ```python 43 | # src/app_module.py 44 | 45 | from nest.core import Module, PyNestFactory 46 | from .app_controller import AppController 47 | from .app_service import AppService 48 | 49 | @Module( 50 | controllers=[AppController], 51 | providers=[AppService], 52 | ) 53 | class AppModule: 54 | pass 55 | 56 | app = PyNestFactory.create( 57 | AppModule, 58 | description="This is my PyNest app", 59 | title="My App", 60 | version="1.0.0", 61 | debug=True, 62 | ) 63 | 64 | http_server = app.get_server() 65 | ``` 66 | 67 | ### Step 2: Create app_service.py 68 | 69 | The app_service.py file will contain the logic for our service. Create the file and add the following code: 70 | 71 | ```python 72 | # src/app_service.py 73 | 74 | from nest.core import Injectable 75 | 76 | @Injectable 77 | class AppService: 78 | def __init__(self): 79 | self.app_name = "MyApp" 80 | self.app_version = "1.0.0" 81 | 82 | def get_app_info(self): 83 | return {"app_name": self.app_name, "app_version": self.app_version} 84 | ``` 85 | 86 | ### Step 3: Create app_controller.py 87 | 88 | The app_controller.py file will handle the routing and responses. Create the file and add the following code: 89 | 90 | ```python 91 | # src/app_controller.py 92 | 93 | from nest.core import Controller, Get 94 | from .app_service import AppService 95 | 96 | @Controller("/") 97 | class AppController: 98 | def __init__(self, service: AppService): 99 | self.service = service 100 | 101 | @Get("/") 102 | def get_app_info(self): 103 | return self.service.get_app_info() 104 | ``` 105 | 106 | ### Step 4: Create main.py 107 | 108 | The main.py file will run our PyNest application. Create the file and add the following code: 109 | 110 | ```python 111 | # main.py 112 | 113 | import uvicorn 114 | from src.app_module import http_server 115 | 116 | if __name__ == "__main__": 117 | uvicorn.run(http_server, host="0.0.0.0", port=8000, reload=True) 118 | ``` 119 | 120 | ### File Structure 🗂️ 121 | 122 | Here's the file structure of your PyNest application after following the steps: 123 | 124 | ```text 125 | my_pynest_project/ 126 | ├── src/ 127 | │ ├── __init__.py 128 | │ ├── app_module.py 129 | │ ├── app_controller.py 130 | │ ├── app_service.py 131 | ├── main.py 132 | ├── requirements.txt 133 | └── README.md 134 | ``` 135 | 136 | ### Running the Application ▶️ 137 | 138 | To run your application, execute the following command: 139 | 140 | ```bash 141 | python main.py 142 | ``` 143 | 144 | You should see the Uvicorn server starting, and you can access your API at . 145 | 146 | --- 147 | -------------------------------------------------------------------------------- /docs/imgs/DI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/docs/imgs/DI.png -------------------------------------------------------------------------------- /docs/imgs/module-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/docs/imgs/module-architecture.png -------------------------------------------------------------------------------- /docs/imgs/pynest-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/docs/imgs/pynest-logo.png -------------------------------------------------------------------------------- /docs/imgs/pynest_new_logo_opt-removebg-preview.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/docs/imgs/pynest_new_logo_opt-removebg-preview.jpeg -------------------------------------------------------------------------------- /docs/imgs/pynest_new_logo_opt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/docs/imgs/pynest_new_logo_opt.png -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Overview 🚀 2 | 3 | Welcome to **PyNest**! PyNest is a Python framework built on top of FastAPI that follows the modular architecture of NestJS. It's designed to help you structure your APIs in an intuitive, easy-to-understand, and enjoyable way. 4 | 5 | With PyNest, you can build scalable and maintainable APIs with ease. The framework supports dependency injection, type annotations, decorators, and code generation, making it easy to write clean and testable code. 6 | 7 | This framework is not a direct port of NestJS to Python but rather a re-imagining of the framework specifically for Python developers, including data scientists, data analysts, and data engineers. It aims to assist them in building better and faster APIs for their data applications. 8 | 9 | ## Key Features ✨ 10 | 11 | ### Modular Architecture 🏗️ 12 | 13 | PyNest follows the modular architecture of NestJS, which allows for easy separation of concerns and code organization. Each module contains a collection of related controllers, services, and providers. 14 | 15 | ### Dependency Injection 💉 16 | 17 | PyNest supports dependency injection, which makes it easy to manage dependencies and write testable code. You can easily inject services and providers into your controllers using decorators. 18 | 19 | ### Decorators 🏷️ 20 | 21 | PyNest makes extensive use of decorators to define routes, middleware, and other application components. This helps keep the code concise and easy to read. 22 | 23 | ### Type Annotations 📝 24 | 25 | PyNest leverages Python's type annotations to provide better tooling and help prevent errors. You can annotate your controllers, services, and providers with types to make your code more robust. 26 | 27 | ### Code Generation ⚙️ 28 | 29 | PyNest includes a code generation tool that can create boilerplate code for modules, controllers, and other components. This saves you time and helps you focus on writing the code that matters. 30 | 31 | ## Contributing 🤝 32 | 33 | Contributions are welcome! Please feel free to submit a Pull Request. 34 | 35 | ## Credits 💡 36 | 37 | - [NestJS](https://nestjs.com/), a framework for building Node.js applications. 38 | - [FastAPI](https://fastapi.tiangolo.com/), a framework for building Python applications. 39 | 40 | --- 41 | 42 | 47 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | PyNest is [MIT licensed](https://github.com/PythonNest/PyNest/blob/main/LICENSE). 4 | 5 | -------------------------------------------------------------------------------- /docs/lifespan_tasks.md: -------------------------------------------------------------------------------- 1 | # Lifaspan tasks in PyNest 2 | 3 | ## Introduction 4 | 5 | Lifespan tasks - coroutines, which run while app is working. 6 | 7 | ## Defining a lifespan task 8 | As example of lifespan task will use coroutine, which print time every hour. In real user cases can be everything else. 9 | 10 | ```python 11 | import asyncio 12 | from datetime import datetime 13 | 14 | async def print_current_time(): 15 | while True: 16 | current_time = datetime.now().strftime("%H:%M:%S") 17 | print(f"Current time: {current_time}") 18 | await asyncio.sleep(3600) 19 | ``` 20 | 21 | ## Implement a lifespan task 22 | In `app_module.py` we can define a startup handler, and run lifespan inside it 23 | 24 | ```python 25 | from nest.core import PyNestFactory 26 | 27 | app = PyNestFactory.create( 28 | AppModule, 29 | description="This is my PyNest app with lifespan task", 30 | title="My App", 31 | version="1.0.0", 32 | debug=True, 33 | ) 34 | 35 | http_server = app.get_server() 36 | 37 | @http_server.on_event("startup") 38 | async def startup(): 39 | await print_current_time() 40 | ``` 41 | 42 | Now `print_current_time` will work in lifespan after startup. 43 | -------------------------------------------------------------------------------- /docs/modules.md: -------------------------------------------------------------------------------- 1 | # Modules in PyNest 📦 2 | 3 | A module is a class annotated with a `@Module()` decorator. The `@Module()` decorator provides metadata that PyNest uses to organize the application structure. 4 | 5 | Each application has at least one module, a root module. 6 | The root module is the starting point PyNest uses 7 | to build the application-graph which is the internal data structure PyNest uses 8 | to resolve module and provider relationships and dependencies. 9 | While very small applications may theoretically have just the root module, this is not the typical case. 10 | Modules are strongly recommended as an effective way to organize your components. 11 | Thus, for most applications, the resulting architecture will employ multiple modules, 12 | each encapsulating a closely related set of capabilities. 13 | 14 | ## Defining a Module 15 | 16 | The `@Module()` decorator takes a single object whose properties describe the module: 17 | 18 | - **providers**: The providers that will be instantiated by the PyNest injector and that may be shared across this module. 19 | - **controllers**: The set of controllers defined in this module which have to be instantiated. 20 | - **imports**: The list of imported modules that export the providers which are required in this module. 21 | - **exports**: The subset of providers that are provided by this module and should be available in other modules that import this module. You can use either the provider itself or just its token (provide value). 22 | 23 | --- 24 | 25 | ![img.png](imgs/module-architecture.png) 26 | > This diagram shows the relationship between modules, providers, and controllers in a PyNest application. 27 | 28 | --- 29 | 30 | **Code Example** 31 | 32 | ```python 33 | from pynest.core import Module 34 | from .book_controller import BookController 35 | from .book_service import BookService 36 | 37 | @Module( 38 | providers=[BookService], 39 | controllers=[BookController], 40 | exports=[BookService], 41 | ) 42 | class BookModule: 43 | pass 44 | ``` 45 | 46 | In the example above, we define a `BookModule` module that exports the `BookService`. 47 | This means that all the routes in the controllers will be registered in the application, 48 | and the `BookService` will be available for injection in other modules that import the `BookModule`. 49 | 50 | ## Module Interactions 51 | Modules interact with each other primarily through their exports and imports. 52 | A module can use a provider from another module if the provider is exported by the first module 53 | and the second module imports it. 54 | 55 | ### Example 56 | ```python 57 | from pynest.core import Module 58 | from .book_module import BookModule 59 | 60 | @Module( 61 | imports=[BookModule], 62 | ) 63 | class AppModule: 64 | pass 65 | ``` 66 | 67 | In this example, the `AppModule` imports the `BookModule`, 68 | which means that all the providers exported by the `BookModule` are available in the 69 | `AppModule` and all the routes in `BookController` will be registered to the main application. 70 | 71 | 72 | --- 73 | 74 | -------------------------------------------------------------------------------- /docs/providers.md: -------------------------------------------------------------------------------- 1 | # Providers and Injectables in PyNest 🏗️ 2 | 3 | Providers, also known as services or injectables, are fundamental in PyNest applications. They handle business logic and other functionalities, acting as dependencies that can be injected into other components like controllers. This guide explains what providers are, how to use them, how to import and export them in modules, and how to inject them into controllers and other services, with various examples. 4 | 5 | ## What is a Provider? 6 | 7 | In PyNest, a provider is a class annotated with the `@Injectable` decorator. This decorator makes the class available for dependency injection, allowing it to be injected into other classes such as controllers and services. 8 | 9 | ## Defining a Provider 10 | 11 | To define a provider, use the `@Injectable` decorator. This makes the class available for dependency injection within the module or globally if exported. 12 | 13 | **Example: Logger Service** 14 | 15 | `logger_service.py` 16 | 17 | ```python 18 | from nest.core import Injectable 19 | 20 | @Injectable 21 | class LoggerService: 22 | def log(self, message: str): 23 | print(f"LOG: {message}") 24 | ``` 25 | 26 | In this example, the `LoggerService` class is defined as a provider by using the `@Injectable` decorator. This class can now be injected into other classes within the same module or exported to be used in other modules. 27 | 28 | ## Using Providers in Modules 29 | 30 | [Modules](modules.md) are used to group related providers, controllers, and other modules. 31 | Providers can be imported and exported in modules to be used across the application. 32 | 33 | **Example: S3 Module** 34 | `s3_module.py` 35 | 36 | ```python 37 | from nest.core import Module 38 | from .s3_service import S3Service 39 | from src.providers.logger.logger_service import LoggerService 40 | from src.providers.logger.logger_module import LoggerModule 41 | 42 | 43 | @Module( 44 | providers=[S3Service, LoggerService], 45 | exports=[S3Service], 46 | imports=[LoggerModule], 47 | ) 48 | class S3Module: 49 | pass 50 | ``` 51 | 52 | In this example: 53 | 54 | The S3Module defines a module that provides the S3Service and exports it for use in other modules. 55 | It is also importing the LoggerModule to use the LoggerService. 56 | 57 | ## Injecting Providers into Controllers and Services 58 | 59 | Providers can be injected into controllers and other services using dependency injection. This is done by declaring the dependency in the constructor of the class where the provider is needed. 60 | 61 | **Example: S3 Service and Controller** 62 | 63 | `s3_service.py` 64 | 65 | ```python 66 | from nest.core import Injectable 67 | from src.providers.logger.logger_service import LoggerService 68 | 69 | @Injectable 70 | class S3Service: 71 | 72 | def __init__(self, logger_service: LoggerService): 73 | self.logger_service = logger_service 74 | 75 | def upload_file(self, file_name: str): 76 | print(f"Uploading {file_name}") 77 | self.logger_service.log(f"Uploaded file: {file_name}") 78 | ``` 79 | 80 | In this Service, the `LoggerService` is injected into the `S3Service` via the constructor. 81 | 82 | `s3_controller.py` 83 | 84 | ```python 85 | from nest.core import Controller, Post 86 | from .s3_service import S3Service 87 | 88 | @Controller('/s3') 89 | class S3Controller: 90 | def __init__(self, s3_service: S3Service): 91 | self.s3_service = s3_service 92 | 93 | @Post('/upload') 94 | def upload_file(self, file_name: str): 95 | self.s3_service.upload_file(file_name) 96 | return {"message": "File uploaded successfully!"} 97 | ``` 98 | 99 | In these examples: 100 | 101 | The S3Service is injected into the S3Controller via the constructor. 102 | 103 | ## Importing and Exporting Providers 104 | Providers can be shared between modules by importing and exporting them. 105 | 106 | **Example: Email Module** 107 | 108 | `email_module.py` 109 | 110 | ```python 111 | from nest.core import Module 112 | from src.providers.s3.s3_module import S3Module 113 | from src.providers.s3.s3_service import S3Service 114 | from .email_service import EmailService 115 | 116 | @Module( 117 | imports=[S3Module], 118 | providers=[EmailService, S3Service], 119 | exports=[EmailService], 120 | ) 121 | class EmailModule: 122 | pass 123 | ``` 124 | 125 | In this example: 126 | 127 | The EmailModule imports the S3Module to use the S3Service provided by it. 128 | 129 | The EmailService is defined as a provider in the EmailModule and exported for use in other modules. 130 | 131 | ## Conclusion 132 | 133 | Providers are essential parts in PyNest applications, handling business logic and other functionalities. 134 | By defining, importing, exporting, 135 | and injecting providers, you can create modular and maintainable applications with clear separation of concerns. 136 | Understanding how providers work and how to use them effectively will help 137 | you build robust and scalable applications with PyNest. 138 | 139 | --- 140 | 141 | -------------------------------------------------------------------------------- /docs/styles/extra.css: -------------------------------------------------------------------------------- 1 | nav.md-footer-nav { 2 | display: flex; 3 | justify-content: space-between; 4 | padding: 1rem 0; 5 | } 6 | 7 | nav.md-footer-nav a.md-footer-nav__link { 8 | display: flex; 9 | align-items: center; 10 | } 11 | 12 | nav.md-footer-nav a.md-footer-nav__link span { 13 | display: flex; 14 | align-items: center; 15 | } 16 | 17 | .md-header__button.md-logo img, .md-header__button.md-logo svg { 18 | height: 2rem; 19 | width: 2rem; 20 | } 21 | .md-header__button.md-logo { 22 | margin: 0; 23 | padding: 0; 24 | } -------------------------------------------------------------------------------- /examples/BlankApp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/BlankApp/__init__.py -------------------------------------------------------------------------------- /examples/BlankApp/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.run("src.app_module:http_server", host="0.0.0.0", port=8010, reload=True) 5 | -------------------------------------------------------------------------------- /examples/BlankApp/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/BlankApp/src/__init__.py -------------------------------------------------------------------------------- /examples/BlankApp/src/app_controller.py: -------------------------------------------------------------------------------- 1 | from nest.core import Controller, Get 2 | 3 | from .app_service import AppService 4 | 5 | 6 | @Controller("/") 7 | class AppController: 8 | def __init__(self, service: AppService): 9 | self.service = service 10 | 11 | @Get("/") 12 | def get_app_info(self): 13 | return self.service.get_app_info() 14 | -------------------------------------------------------------------------------- /examples/BlankApp/src/app_module.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from nest.core import Module, PyNestFactory 4 | 5 | from .app_controller import AppController 6 | from .app_service import AppService 7 | from .example.example_module import ExampleModule 8 | from .product.product_module import ProductModule 9 | from .user.user_module import UserModule 10 | 11 | 12 | @Module( 13 | imports=[ExampleModule, UserModule, ProductModule], 14 | controllers=[AppController], 15 | providers=[AppService], 16 | ) 17 | class AppModule: 18 | pass 19 | 20 | 21 | app = PyNestFactory.create( 22 | AppModule, 23 | description="This is my FastAPI app.", 24 | title="My App", 25 | version="1.0.0", 26 | debug=True, 27 | ) 28 | 29 | http_server: FastAPI = app.get_server() 30 | -------------------------------------------------------------------------------- /examples/BlankApp/src/app_service.py: -------------------------------------------------------------------------------- 1 | from nest.core import Injectable 2 | 3 | 4 | @Injectable 5 | class AppService: 6 | def __init__(self): 7 | self.app_name = "BlankApp" 8 | self.app_version = "1.0.0" 9 | 10 | def get_app_info(self): 11 | return {"app_name": self.app_name, "app_version": self.app_version} 12 | -------------------------------------------------------------------------------- /examples/BlankApp/src/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/BlankApp/src/example/__init__.py -------------------------------------------------------------------------------- /examples/BlankApp/src/example/example_controller.py: -------------------------------------------------------------------------------- 1 | from nest.core import Controller, Get, HttpCode, Post 2 | 3 | from .example_model import Example 4 | from .example_service import ExampleService 5 | 6 | 7 | @Controller("example") 8 | class ExampleController: 9 | def __init__(self, service: ExampleService): 10 | self.service = service 11 | 12 | @Get("/") 13 | def get_example(self): 14 | return self.service.get_example() 15 | 16 | @Post("/") 17 | @HttpCode(201) 18 | def add_example(self, example: Example): 19 | return self.service.add_example(example) 20 | -------------------------------------------------------------------------------- /examples/BlankApp/src/example/example_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Example(BaseModel): 5 | name: str 6 | -------------------------------------------------------------------------------- /examples/BlankApp/src/example/example_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module 2 | 3 | from .example_controller import ExampleController 4 | from .example_service import ExampleService 5 | 6 | 7 | @Module(controllers=[ExampleController], providers=[ExampleService], imports=[]) 8 | class ExampleModule: 9 | pass 10 | -------------------------------------------------------------------------------- /examples/BlankApp/src/example/example_service.py: -------------------------------------------------------------------------------- 1 | from nest.core.decorators import Injectable 2 | 3 | from ..user.user_service import UserService 4 | from .example_model import Example 5 | 6 | 7 | @Injectable 8 | class ExampleService: 9 | def __init__(self, user_service: UserService): 10 | self.database = [] 11 | self.user_service = user_service 12 | 13 | def get_example(self): 14 | return self.database 15 | 16 | def add_example(self, example: Example): 17 | self.database.append(example) 18 | return example 19 | -------------------------------------------------------------------------------- /examples/BlankApp/src/product/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/BlankApp/src/product/__init__.py -------------------------------------------------------------------------------- /examples/BlankApp/src/product/product_controller.py: -------------------------------------------------------------------------------- 1 | from nest.core import Controller, Get, Post 2 | 3 | from .product_model import Product 4 | from .product_service import ProductService 5 | 6 | 7 | @Controller("product") 8 | class ProductController: 9 | def __init__(self, service: ProductService): 10 | self.service = service 11 | 12 | @Get("/") 13 | def get_product(self): 14 | return self.service.get_product() 15 | 16 | @Post("/") 17 | def add_product(self, product: Product): 18 | return self.service.add_product(product) 19 | -------------------------------------------------------------------------------- /examples/BlankApp/src/product/product_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Product(BaseModel): 5 | name: str 6 | -------------------------------------------------------------------------------- /examples/BlankApp/src/product/product_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module 2 | 3 | from .product_controller import ProductController 4 | from .product_service import ProductService 5 | 6 | 7 | @Module(controllers=[ProductController], providers=[ProductService], imports=[]) 8 | class ProductModule: 9 | pass 10 | -------------------------------------------------------------------------------- /examples/BlankApp/src/product/product_service.py: -------------------------------------------------------------------------------- 1 | from nest.core.decorators import Injectable 2 | 3 | from .product_model import Product 4 | 5 | 6 | @Injectable 7 | class ProductService: 8 | def __init__(self): 9 | self.database = [] 10 | 11 | def get_product(self): 12 | return self.database 13 | 14 | def add_product(self, product: Product): 15 | self.database.append(product) 16 | return product 17 | -------------------------------------------------------------------------------- /examples/BlankApp/src/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/BlankApp/src/user/__init__.py -------------------------------------------------------------------------------- /examples/BlankApp/src/user/user_controller.py: -------------------------------------------------------------------------------- 1 | from nest.core import Controller, Depends, Get, Post 2 | 3 | from .user_model import User 4 | from .user_service import UserService 5 | 6 | 7 | @Controller("user") 8 | class UserController: 9 | def __init__(self, service: UserService): 10 | self.service = service 11 | 12 | @Get("/") 13 | def get_user(self): 14 | return self.service.get_user() 15 | 16 | @Post("/") 17 | def add_user(self, user: User): 18 | return self.service.add_user(user) 19 | -------------------------------------------------------------------------------- /examples/BlankApp/src/user/user_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class User(BaseModel): 5 | name: str 6 | -------------------------------------------------------------------------------- /examples/BlankApp/src/user/user_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module 2 | 3 | from .user_controller import UserController 4 | from .user_service import UserService 5 | 6 | 7 | @Module(controllers=[UserController], providers=[UserService], imports=[]) 8 | class UserModule: 9 | pass 10 | -------------------------------------------------------------------------------- /examples/BlankApp/src/user/user_service.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from nest.core.decorators import Injectable 4 | 5 | from .user_model import User 6 | 7 | 8 | @Injectable 9 | class UserService: 10 | def __init__(self): 11 | self.database = [] 12 | time.sleep(5) 13 | print("UserService initialized") 14 | 15 | def get_user(self): 16 | return self.database 17 | 18 | def add_user(self, user: User): 19 | self.database.append(user) 20 | return user 21 | -------------------------------------------------------------------------------- /examples/CommandLineApp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/CommandLineApp/__init__.py -------------------------------------------------------------------------------- /examples/CommandLineApp/main.py: -------------------------------------------------------------------------------- 1 | from src.app_module import app 2 | 3 | if __name__ == "__main__": 4 | app() 5 | -------------------------------------------------------------------------------- /examples/CommandLineApp/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/CommandLineApp/src/__init__.py -------------------------------------------------------------------------------- /examples/CommandLineApp/src/app_controller.py: -------------------------------------------------------------------------------- 1 | from nest.core.decorators.cli.cli_decorators import CliCommand, CliController 2 | 3 | from .app_service import AppService 4 | 5 | 6 | @CliController("app") 7 | class AppController: 8 | 9 | def __init__(self, app_service: AppService): 10 | self.app_service = app_service 11 | 12 | @CliCommand("info") 13 | def get_app_info(self): 14 | app_info = self.app_service.get_app_info() 15 | print(app_info) 16 | -------------------------------------------------------------------------------- /examples/CommandLineApp/src/app_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module 2 | from nest.core.cli_factory import CLIAppFactory 3 | from nest.core.pynest_factory import PyNestFactory 4 | 5 | from .app_controller import AppController 6 | from .app_service import AppService 7 | from .user.user_module import UserModule 8 | from .user.user_service import UserService 9 | 10 | 11 | @Module( 12 | imports=[UserModule], 13 | controllers=[AppController], 14 | providers=[UserService, AppService], 15 | ) 16 | class AppModule: 17 | pass 18 | 19 | 20 | app = CLIAppFactory().create(AppModule) 21 | -------------------------------------------------------------------------------- /examples/CommandLineApp/src/app_service.py: -------------------------------------------------------------------------------- 1 | from nest.core import Injectable 2 | 3 | 4 | @Injectable 5 | class AppService: 6 | def __init__(self): 7 | self.app_name = "Pynest App" 8 | self.app_version = "1.0.0" 9 | 10 | def get_app_info(self): 11 | return {"app_name": self.app_name, "app_version": self.app_version} 12 | -------------------------------------------------------------------------------- /examples/CommandLineApp/src/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/CommandLineApp/src/user/__init__.py -------------------------------------------------------------------------------- /examples/CommandLineApp/src/user/user_controller.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from examples.CommandLineApp.src.user.user_service import UserService 4 | from nest.core.decorators.cli.cli_decorators import CliCommand, CliController 5 | 6 | 7 | class ListOptions: 8 | USER = click.Option( 9 | ["-u", "--user"], help="user name to retrieve", required=True, type=str 10 | ) 11 | 12 | 13 | @CliController("user") 14 | class UserController: 15 | 16 | def __init__(self, user_service: UserService): 17 | self.user_service = user_service 18 | 19 | @CliCommand("list", help="List all users") 20 | def list_users(self): 21 | return self.user_service.get_users() 22 | 23 | @CliCommand("show", help="Show user by id") 24 | def show_user( 25 | self, 26 | user: ListOptions.USER, 27 | ): 28 | return self.user_service.get_user(user) 29 | -------------------------------------------------------------------------------- /examples/CommandLineApp/src/user/user_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module 2 | 3 | from .user_controller import UserController 4 | from .user_service import UserService 5 | 6 | 7 | @Module(controllers=[UserController], providers=[UserService]) 8 | class UserModule: 9 | pass 10 | -------------------------------------------------------------------------------- /examples/CommandLineApp/src/user/user_service.py: -------------------------------------------------------------------------------- 1 | from nest.core import Injectable 2 | 3 | 4 | @Injectable 5 | class UserService: 6 | 7 | def get_users(self): 8 | print("This command show all users") 9 | 10 | def get_user(self, user_name: str): 11 | print(f"This command show user with user_name: {user_name}") 12 | -------------------------------------------------------------------------------- /examples/MongoApp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/MongoApp/__init__.py -------------------------------------------------------------------------------- /examples/MongoApp/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.run("src.app_module:http_server", host="0.0.0.0", port=8000, reload=True) 5 | -------------------------------------------------------------------------------- /examples/MongoApp/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/MongoApp/src/__init__.py -------------------------------------------------------------------------------- /examples/MongoApp/src/app_controller.py: -------------------------------------------------------------------------------- 1 | from nest.core import Controller, Get 2 | 3 | from .app_service import AppService 4 | 5 | 6 | @Controller("/") 7 | class AppController: 8 | def __init__(self, service: AppService): 9 | self.service = service 10 | 11 | @Get("/") 12 | def get_app_info(self): 13 | return self.service.get_app_info() 14 | -------------------------------------------------------------------------------- /examples/MongoApp/src/app_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module, PyNestFactory 2 | 3 | from .app_controller import AppController 4 | from .app_service import AppService 5 | from .config import config 6 | from .example.example_module import ExampleModule 7 | from .product.product_module import ProductModule 8 | from .user.user_module import UserModule 9 | 10 | 11 | @Module( 12 | imports=[ExampleModule, UserModule, ProductModule], 13 | controllers=[AppController], 14 | providers=[AppService], 15 | ) 16 | class AppModule: 17 | pass 18 | 19 | 20 | app = PyNestFactory.create( 21 | AppModule, 22 | description="This is my FastAPI app drive by MongoDB Engine", 23 | title="My App", 24 | version="1.0.0", 25 | debug=True, 26 | ) 27 | 28 | http_server = app.get_server() 29 | 30 | 31 | @http_server.on_event("startup") 32 | async def startup(): 33 | await config.create_all() 34 | -------------------------------------------------------------------------------- /examples/MongoApp/src/app_service.py: -------------------------------------------------------------------------------- 1 | from nest.core import Injectable 2 | 3 | 4 | @Injectable 5 | class AppService: 6 | def __init__(self): 7 | self.app_name = "MongoApp" 8 | self.app_version = "1.0.0" 9 | 10 | def get_app_info(self): 11 | return {"app_name": self.app_name, "app_version": self.app_version} 12 | -------------------------------------------------------------------------------- /examples/MongoApp/src/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | from nest.core.database.odm_provider import OdmProvider 6 | 7 | from .example.example_entity import Example 8 | from .product.product_entity import Product 9 | from .user.user_entity import User 10 | 11 | load_dotenv() 12 | config = OdmProvider( 13 | config_params={ 14 | "db_name": os.getenv("DB_NAME", "default_nest_db"), 15 | "host": os.getenv("DB_HOST", "localhost"), 16 | "port": os.getenv("DB_PORT", 27017), 17 | }, 18 | document_models=[User, Product, Example], 19 | ) 20 | -------------------------------------------------------------------------------- /examples/MongoApp/src/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/MongoApp/src/example/__init__.py -------------------------------------------------------------------------------- /examples/MongoApp/src/example/example_controller.py: -------------------------------------------------------------------------------- 1 | from nest.core import Controller, Get, Post 2 | 3 | from .example_model import Example 4 | from .example_service import ExampleService 5 | 6 | 7 | @Controller("example") 8 | class ExampleController: 9 | def __init__(self, service: ExampleService): 10 | self.service = service 11 | 12 | @Get("/") 13 | async def get_example(self): 14 | return await self.service.get_example() 15 | 16 | @Post("/") 17 | async def add_example(self, example: Example): 18 | return await self.service.add_example(example) 19 | -------------------------------------------------------------------------------- /examples/MongoApp/src/example/example_entity.py: -------------------------------------------------------------------------------- 1 | from beanie import Document 2 | 3 | 4 | class Example(Document): 5 | name: str 6 | 7 | class Config: 8 | schema_extra = { 9 | "example": { 10 | "name": "Example Name", 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/MongoApp/src/example/example_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Example(BaseModel): 5 | name: str 6 | -------------------------------------------------------------------------------- /examples/MongoApp/src/example/example_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module 2 | 3 | from .example_controller import ExampleController 4 | from .example_service import ExampleService 5 | 6 | 7 | @Module(controllers=[ExampleController], providers=[ExampleService], imports=[]) 8 | class ExampleModule: 9 | pass 10 | -------------------------------------------------------------------------------- /examples/MongoApp/src/example/example_service.py: -------------------------------------------------------------------------------- 1 | from nest.core import Injectable 2 | from nest.core.decorators.database import db_request_handler 3 | 4 | from .example_entity import Example as ExampleEntity 5 | from .example_model import Example 6 | 7 | # import asyncio # Uncomment this line to test the async nature of the service 8 | 9 | 10 | @Injectable 11 | class ExampleService: 12 | @db_request_handler 13 | async def add_example(self, example: Example): 14 | new_example = ExampleEntity(**example.dict()) 15 | await new_example.save() 16 | return new_example.id 17 | 18 | @db_request_handler 19 | async def get_example(self): 20 | # await asyncio.sleep(5) 21 | return await ExampleEntity.find_all().to_list() 22 | -------------------------------------------------------------------------------- /examples/MongoApp/src/product/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/MongoApp/src/product/__init__.py -------------------------------------------------------------------------------- /examples/MongoApp/src/product/product_controller.py: -------------------------------------------------------------------------------- 1 | from nest.core import Controller, Depends, Get, Post 2 | 3 | from .product_model import Product 4 | from .product_service import ProductService 5 | 6 | 7 | @Controller("product") 8 | class ProductController: 9 | def __init__(self, service: ProductService): 10 | self.service = service 11 | 12 | @Get("/") 13 | async def get_product(self): 14 | return await self.service.get_product() 15 | 16 | @Post("/") 17 | async def add_product(self, product: Product): 18 | return await self.service.add_product(product) 19 | -------------------------------------------------------------------------------- /examples/MongoApp/src/product/product_entity.py: -------------------------------------------------------------------------------- 1 | from beanie import Document 2 | 3 | 4 | class Product(Document): 5 | name: str 6 | 7 | class Config: 8 | schema_extra = { 9 | "example": { 10 | "name": "Example Name", 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/MongoApp/src/product/product_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Product(BaseModel): 5 | name: str 6 | -------------------------------------------------------------------------------- /examples/MongoApp/src/product/product_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module 2 | 3 | from .product_controller import ProductController 4 | from .product_service import ProductService 5 | 6 | 7 | @Module(controllers=[ProductController], providers=[ProductService], imports=[]) 8 | class ProductModule: 9 | pass 10 | -------------------------------------------------------------------------------- /examples/MongoApp/src/product/product_service.py: -------------------------------------------------------------------------------- 1 | from nest.core import Injectable 2 | from nest.core.decorators.database import db_request_handler 3 | 4 | from .product_entity import Product as ProductEntity 5 | from .product_model import Product 6 | 7 | 8 | @Injectable 9 | class ProductService: 10 | @db_request_handler 11 | async def add_product(self, product: Product): 12 | new_product = ProductEntity(**product.dict()) 13 | await new_product.save() 14 | return new_product.id 15 | 16 | @db_request_handler 17 | async def get_product(self): 18 | return await ProductEntity.find_all().to_list() 19 | -------------------------------------------------------------------------------- /examples/MongoApp/src/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/MongoApp/src/user/__init__.py -------------------------------------------------------------------------------- /examples/MongoApp/src/user/user_controller.py: -------------------------------------------------------------------------------- 1 | from nest.core import Controller, Depends, Get, Post 2 | 3 | from .user_model import User 4 | from .user_service import UserService 5 | 6 | 7 | @Controller("user") 8 | class UserController: 9 | def __init__(self, service: UserService): 10 | self.service = service 11 | 12 | @Get("/") 13 | async def get_user(self): 14 | return await self.service.get_user() 15 | 16 | @Post("/") 17 | async def add_user(self, user: User): 18 | return await self.service.add_user(user) 19 | -------------------------------------------------------------------------------- /examples/MongoApp/src/user/user_entity.py: -------------------------------------------------------------------------------- 1 | from beanie import Document 2 | 3 | 4 | class User(Document): 5 | name: str 6 | 7 | class Config: 8 | schema_extra = { 9 | "example": { 10 | "name": "Example Name", 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/MongoApp/src/user/user_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class User(BaseModel): 5 | name: str 6 | -------------------------------------------------------------------------------- /examples/MongoApp/src/user/user_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module 2 | 3 | from .user_controller import UserController 4 | from .user_service import UserService 5 | 6 | 7 | @Module(controllers=[UserController], providers=[UserService], imports=[]) 8 | class UserModule: 9 | pass 10 | -------------------------------------------------------------------------------- /examples/MongoApp/src/user/user_service.py: -------------------------------------------------------------------------------- 1 | from nest.core import Injectable 2 | from nest.core.decorators.database import db_request_handler 3 | 4 | from .user_entity import User as UserEntity 5 | from .user_model import User 6 | 7 | 8 | @Injectable 9 | class UserService: 10 | @db_request_handler 11 | async def add_user(self, user: User): 12 | new_user = UserEntity(**user.dict()) 13 | await new_user.save() 14 | return new_user.id 15 | 16 | @db_request_handler 17 | async def get_user(self): 18 | return await UserEntity.find_all().to_list() 19 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/OrmAsyncApp/__init__.py -------------------------------------------------------------------------------- /examples/OrmAsyncApp/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.run("src.app_module:http_server", host="0.0.0.0", port=8001, reload=True) 5 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/OrmAsyncApp/src/__init__.py -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/app_controller.py: -------------------------------------------------------------------------------- 1 | from nest.core import Controller, Get 2 | 3 | from .app_service import AppService 4 | 5 | 6 | @Controller("/") 7 | class AppController: 8 | def __init__(self, service: AppService): 9 | self.service = service 10 | 11 | @Get("/") 12 | async def get_app_info(self): 13 | return await self.service.get_app_info() 14 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/app_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module, PyNestFactory 2 | 3 | from .app_controller import AppController 4 | from .app_service import AppService 5 | from .config import config 6 | from .example.example_module import ExampleModule 7 | from .product.product_module import ProductModule 8 | from .user.user_module import UserModule 9 | 10 | 11 | @Module( 12 | imports=[ExampleModule, UserModule, ProductModule], 13 | controllers=[AppController], 14 | providers=[AppService], 15 | ) 16 | class AppModule: 17 | pass 18 | 19 | 20 | app = PyNestFactory.create( 21 | AppModule, 22 | description="This is my FastAPI app drive by Async ORM Engine", 23 | title="My App", 24 | version="1.0.0", 25 | debug=True, 26 | ) 27 | 28 | http_server = app.get_server() 29 | 30 | 31 | @http_server.on_event("startup") 32 | async def startup(): 33 | await config.create_all() 34 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/app_service.py: -------------------------------------------------------------------------------- 1 | from nest.core import Injectable 2 | 3 | 4 | @Injectable 5 | class AppService: 6 | def __init__(self): 7 | self.app_name = "MongoApp" 8 | self.app_version = "1.0.0" 9 | 10 | async def get_app_info(self): 11 | return {"app_name": self.app_name, "app_version": self.app_version} 12 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | from nest.core.database.orm_provider import AsyncOrmProvider 6 | 7 | load_dotenv() 8 | 9 | config = AsyncOrmProvider( 10 | db_type="sqlite", 11 | config_params=dict( 12 | db_name=os.getenv("SQLITE_DB_NAME", "default_nest_db"), 13 | ), 14 | ) 15 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/OrmAsyncApp/src/example/__init__.py -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/example/example_controller.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.asyncio import AsyncSession 2 | 3 | from nest.core import Controller, Depends, Get, Post 4 | 5 | from ..config import config 6 | from .example_model import Example 7 | from .example_service import ExampleService 8 | 9 | 10 | @Controller("example") 11 | class ExampleController: 12 | def __init__(self, service: ExampleService): 13 | self.service = service 14 | 15 | @Get("/") 16 | async def get_example(self, session: AsyncSession = Depends(config.get_db)): 17 | return await self.service.get_example(session) 18 | 19 | @Post("/") 20 | async def add_example( 21 | self, example: Example, session: AsyncSession = Depends(config.get_db) 22 | ): 23 | return await self.service.add_example(example, session) 24 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/example/example_entity.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, String 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | 4 | from ..config import config 5 | 6 | 7 | class Example(config.Base): 8 | __tablename__ = "example" 9 | 10 | id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) 11 | name: Mapped[str] = mapped_column(String, unique=True) 12 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/example/example_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Example(BaseModel): 5 | name: str 6 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/example/example_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module 2 | 3 | from .example_controller import ExampleController 4 | from .example_service import ExampleService 5 | 6 | 7 | @Module(controllers=[ExampleController], providers=[ExampleService], imports=[]) 8 | class ExampleModule: 9 | pass 10 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/example/example_service.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import select 2 | from sqlalchemy.ext.asyncio import AsyncSession 3 | 4 | from nest.core import Injectable 5 | from nest.core.decorators.database import async_db_request_handler 6 | 7 | from .example_entity import Example as ExampleEntity 8 | from .example_model import Example 9 | 10 | 11 | @Injectable 12 | class ExampleService: 13 | @async_db_request_handler 14 | async def add_example(self, example: Example, session: AsyncSession): 15 | new_example = ExampleEntity(**example.dict()) 16 | session.add(new_example) 17 | await session.commit() 18 | return new_example.id 19 | 20 | @async_db_request_handler 21 | async def get_example(self, session: AsyncSession): 22 | query = select(ExampleEntity) 23 | result = await session.execute(query) 24 | return result.scalars().all() 25 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/product/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/OrmAsyncApp/src/product/__init__.py -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/product/product_controller.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.asyncio import AsyncSession 2 | 3 | from nest.core import Controller, Depends, Get, Post 4 | 5 | from ..config import config 6 | from .product_model import Product 7 | from .product_service import ProductService 8 | 9 | 10 | @Controller("product") 11 | class ProductController: 12 | def __init__(self, service: ProductService): 13 | self.service = service 14 | 15 | @Get("/") 16 | async def get_product(self, session: AsyncSession = Depends(config.get_db)): 17 | return await self.service.get_product(session) 18 | 19 | @Post("/") 20 | async def add_product( 21 | self, product: Product, session: AsyncSession = Depends(config.get_db) 22 | ): 23 | return await self.service.add_product(product, session) 24 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/product/product_entity.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, String 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | 4 | from ..config import config 5 | 6 | 7 | class Product(config.Base): 8 | __tablename__ = "product" 9 | 10 | id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) 11 | name: Mapped[str] = mapped_column(String, unique=True) 12 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/product/product_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Product(BaseModel): 5 | name: str 6 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/product/product_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module 2 | 3 | from .product_controller import ProductController 4 | from .product_service import ProductService 5 | 6 | 7 | @Module(controllers=[ProductController], providers=[ProductService], imports=[]) 8 | class ProductModule: 9 | pass 10 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/product/product_service.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import select 2 | from sqlalchemy.ext.asyncio import AsyncSession 3 | 4 | from nest.core import Injectable 5 | from nest.core.decorators.database import async_db_request_handler 6 | 7 | from .product_entity import Product as ProductEntity 8 | from .product_model import Product 9 | 10 | 11 | @Injectable 12 | class ProductService: 13 | @async_db_request_handler 14 | async def add_product(self, product: Product, session: AsyncSession): 15 | new_product = ProductEntity(**product.dict()) 16 | session.add(new_product) 17 | await session.commit() 18 | return new_product.id 19 | 20 | @async_db_request_handler 21 | async def get_product(self, session: AsyncSession): 22 | query = select(ProductEntity) 23 | result = await session.execute(query) 24 | return result.scalars().all() 25 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/OrmAsyncApp/src/user/__init__.py -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/user/user_controller.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.asyncio import AsyncSession 2 | 3 | from nest.core import Controller, Depends, Get, Post 4 | 5 | from ..config import config 6 | from .user_model import User 7 | from .user_service import UserService 8 | 9 | 10 | @Controller("user") 11 | class UserController: 12 | def __init__(self, service: UserService): 13 | self.service = service 14 | 15 | @Get("/") 16 | async def get_user(self, session: AsyncSession = Depends(config.get_db)): 17 | return await self.service.get_user(session) 18 | 19 | @Post("/") 20 | async def add_user( 21 | self, user: User, session: AsyncSession = Depends(config.get_db) 22 | ): 23 | return await self.service.add_user(user, session) 24 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/user/user_entity.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, String 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | 4 | from ..config import config 5 | 6 | 7 | class User(config.Base): 8 | __tablename__ = "user" 9 | 10 | id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) 11 | name: Mapped[str] = mapped_column(String, unique=True) 12 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/user/user_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class User(BaseModel): 5 | name: str 6 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/user/user_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module 2 | 3 | from .user_controller import UserController 4 | from .user_service import UserService 5 | 6 | 7 | @Module(controllers=[UserController], providers=[UserService], imports=[]) 8 | class UserModule: 9 | pass 10 | -------------------------------------------------------------------------------- /examples/OrmAsyncApp/src/user/user_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | To Test the asynchronous database operations, uncomment the commented lines in the code below and run the application. 3 | """ 4 | 5 | from sqlalchemy import select 6 | from sqlalchemy.ext.asyncio import AsyncSession 7 | 8 | from nest.core import Injectable 9 | from nest.core.decorators.database import async_db_request_handler 10 | 11 | from .user_entity import User as UserEntity 12 | from .user_model import User 13 | 14 | # import asyncio 15 | 16 | 17 | @Injectable 18 | class UserService: 19 | @async_db_request_handler 20 | async def add_user(self, user: User, session: AsyncSession): 21 | new_user = UserEntity(**user.dict()) 22 | session.add(new_user) 23 | await session.commit() 24 | return new_user.id 25 | 26 | @async_db_request_handler 27 | async def get_user(self, session: AsyncSession): 28 | query = select(UserEntity) 29 | result = await session.execute(query) 30 | # await asyncio.sleep(5) 31 | return result.scalars().all() 32 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/OrmSyncApp/__init__.py -------------------------------------------------------------------------------- /examples/OrmSyncApp/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.run("src.app_module:http_server", host="0.0.0.0", port=8000, reload=True) 5 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/OrmSyncApp/src/__init__.py -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/app_controller.py: -------------------------------------------------------------------------------- 1 | from nest.core import Controller, Get 2 | 3 | from .app_service import AppService 4 | 5 | 6 | @Controller("/") 7 | class AppController: 8 | def __init__(self, service: AppService): 9 | self.service = service 10 | 11 | @Get("/") 12 | def get_app_info(self): 13 | return self.service.get_app_info() 14 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/app_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module, PyNestFactory 2 | 3 | from .app_controller import AppController 4 | from .app_service import AppService 5 | from .config import config 6 | from .example.example_module import ExampleModule 7 | from .product.product_module import ProductModule 8 | from .user.user_module import UserModule 9 | 10 | 11 | @Module( 12 | imports=[ExampleModule, UserModule, ProductModule], 13 | controllers=[AppController], 14 | providers=[AppService], 15 | ) 16 | class AppModule: 17 | pass 18 | 19 | 20 | app = PyNestFactory.create( 21 | AppModule, 22 | description="This is my FastAPI app drive by Sync ORM Engine", 23 | title="My App", 24 | version="1.0.0", 25 | debug=True, 26 | ) 27 | 28 | http_server = app.get_server() 29 | 30 | 31 | @http_server.on_event("startup") 32 | def startup(): 33 | config.create_all() 34 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/app_service.py: -------------------------------------------------------------------------------- 1 | from nest.core import Injectable 2 | 3 | 4 | @Injectable 5 | class AppService: 6 | def __init__(self): 7 | self.app_name = "SyncORMApp" 8 | self.app_version = "1.0.0" 9 | 10 | def get_app_info(self): 11 | return {"app_name": self.app_name, "app_version": self.app_version} 12 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | from nest.core.database.orm_provider import OrmProvider 6 | 7 | load_dotenv() 8 | 9 | config = OrmProvider( 10 | db_type="sqlite", 11 | config_params=dict( 12 | db_name=os.getenv("SQLITE_DB_NAME", "default_nest_db"), 13 | ), 14 | ) 15 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/OrmSyncApp/src/example/__init__.py -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/example/example_controller.py: -------------------------------------------------------------------------------- 1 | from nest.core import Controller, Depends, Get, Post 2 | 3 | from .example_model import Example 4 | from .example_service import ExampleService 5 | 6 | 7 | @Controller("example") 8 | class ExampleController: 9 | def __init__(self, service: ExampleService): 10 | self.service = service 11 | 12 | @Get("/") 13 | def get_example(self): 14 | return self.service.get_example() 15 | 16 | @Post("/") 17 | def add_example(self, example: Example): 18 | return self.service.add_example(example) 19 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/example/example_entity.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, String 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | 4 | from ..config import config 5 | 6 | 7 | class Example(config.Base): 8 | __tablename__ = "example" 9 | 10 | id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) 11 | name: Mapped[str] = mapped_column(String, unique=True) 12 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/example/example_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Example(BaseModel): 5 | name: str 6 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/example/example_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module 2 | 3 | from .example_controller import ExampleController 4 | from .example_service import ExampleService 5 | 6 | 7 | @Module(controllers=[ExampleController], providers=[ExampleService], imports=[]) 8 | class ExampleModule: 9 | pass 10 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/example/example_service.py: -------------------------------------------------------------------------------- 1 | from nest.core import Injectable 2 | from nest.core.decorators.database import db_request_handler 3 | 4 | from ..config import config 5 | from .example_entity import Example as ExampleEntity 6 | from .example_model import Example 7 | 8 | 9 | @Injectable 10 | class ExampleService: 11 | def __init__(self): 12 | self.config = config 13 | self.session = self.config.get_db() 14 | 15 | @db_request_handler 16 | def add_example(self, example: Example): 17 | new_example = ExampleEntity(**example.dict()) 18 | self.session.add(new_example) 19 | self.session.commit() 20 | return new_example.id 21 | 22 | @db_request_handler 23 | def get_example(self): 24 | return self.session.query(ExampleEntity).all() 25 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/product/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/OrmSyncApp/src/product/__init__.py -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/product/product_controller.py: -------------------------------------------------------------------------------- 1 | from nest.core import Controller, Depends, Get, Post 2 | 3 | from .product_model import Product 4 | from .product_service import ProductService 5 | 6 | 7 | @Controller("product") 8 | class ProductController: 9 | def __init__(self, service: ProductService): 10 | self.service = service 11 | 12 | @Get("/") 13 | def get_products(self): 14 | return self.service.get_products() 15 | 16 | @Post("/") 17 | def add_product(self, product: Product): 18 | return self.service.add_product(product) 19 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/product/product_entity.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, String 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | 4 | from ..config import config 5 | 6 | 7 | class Product(config.Base): 8 | __tablename__ = "product" 9 | 10 | id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) 11 | name: Mapped[str] = mapped_column(String, unique=True) 12 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/product/product_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Product(BaseModel): 5 | name: str 6 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/product/product_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module 2 | 3 | from .product_controller import ProductController 4 | from .product_service import ProductService 5 | 6 | 7 | @Module(controllers=[ProductController], providers=[ProductService], imports=[]) 8 | class ProductModule: 9 | pass 10 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/product/product_service.py: -------------------------------------------------------------------------------- 1 | from nest.core import Injectable 2 | from nest.core.decorators.database import db_request_handler 3 | 4 | from ..config import config 5 | from .product_entity import Product as ProductEntity 6 | from .product_model import Product 7 | 8 | 9 | @Injectable 10 | class ProductService: 11 | def __init__(self): 12 | self.config = config 13 | self.session = self.config.get_db() 14 | 15 | @db_request_handler 16 | def add_product(self, product: Product): 17 | new_product = ProductEntity(**product.dict()) 18 | self.session.add(new_product) 19 | self.session.commit() 20 | return new_product.id 21 | 22 | @db_request_handler 23 | def get_products(self): 24 | return self.session.query(ProductEntity).all() 25 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/OrmSyncApp/src/user/__init__.py -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/user/user_controller.py: -------------------------------------------------------------------------------- 1 | from nest.core import Controller, Depends, Get, Post 2 | 3 | from .user_model import User 4 | from .user_service import UserService 5 | 6 | 7 | @Controller("user") 8 | class UserController: 9 | def __init__(self, service: UserService): 10 | self.service = service 11 | 12 | @Get("/") 13 | def get_users(self): 14 | return self.service.get_users() 15 | 16 | @Post("/") 17 | def add_user(self, user: User): 18 | return self.service.add_user(user) 19 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/user/user_entity.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, String 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | 4 | from ..config import config 5 | 6 | 7 | class User(config.Base): 8 | __tablename__ = "user" 9 | 10 | id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) 11 | name: Mapped[str] = mapped_column(String, unique=True) 12 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/user/user_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class User(BaseModel): 5 | name: str 6 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/user/user_module.py: -------------------------------------------------------------------------------- 1 | from nest.core import Module 2 | 3 | from .user_controller import UserController 4 | from .user_service import UserService 5 | 6 | 7 | @Module(controllers=[UserController], providers=[UserService], imports=[]) 8 | class UserModule: 9 | pass 10 | -------------------------------------------------------------------------------- /examples/OrmSyncApp/src/user/user_service.py: -------------------------------------------------------------------------------- 1 | from nest.core import Injectable 2 | from nest.core.decorators.database import db_request_handler 3 | 4 | from ..config import config 5 | from .user_entity import User as UserEntity 6 | from .user_model import User 7 | 8 | 9 | @Injectable 10 | class UserService: 11 | def __init__(self): 12 | self.config = config 13 | self.session = self.config.get_db() 14 | 15 | @db_request_handler 16 | def add_user(self, user: User): 17 | new_user = UserEntity(**user.dict()) 18 | self.session.add(new_user) 19 | self.session.commit() 20 | return new_user.id 21 | 22 | @db_request_handler 23 | def get_users(self): 24 | return self.session.query(UserEntity).all() 25 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/examples/__init__.py -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: PyNest 2 | 3 | site_url: https://PyNest.github.io/PyNest/ 4 | docs_dir: docs 5 | repo_url: https://github.com/PythonNest/PyNest 6 | repo_name: Pynest 7 | 8 | 9 | theme: 10 | favicon: imgs/pynest-logo.png 11 | name: material 12 | logo: imgs/pynest-logo.png 13 | features: 14 | - content.code.copy 15 | - navigation.tabs 16 | icon: 17 | repo: fontawesome/brands/github-alt 18 | palette: 19 | - media: "(prefers-color-scheme: light)" 20 | scheme: default 21 | primary: teal 22 | accent: amber 23 | toggle: 24 | icon: material/toggle-switch 25 | name: Switch to dark mode 26 | # Palette toggle for dark mode 27 | - media: "(prefers-color-scheme: dark)" 28 | scheme: slate 29 | primary: teal 30 | accent: amber 31 | toggle: 32 | icon: material/toggle-switch-off-outline 33 | name: Switch to light mode 34 | 35 | plugins: 36 | - search 37 | - mkdocstrings 38 | 39 | markdown_extensions: 40 | - pymdownx.highlight: 41 | anchor_linenums: true 42 | - pymdownx.inlinehilite 43 | - pymdownx.snippets 44 | - pymdownx.superfences 45 | 46 | 47 | 48 | extra_css: 49 | - styles/extra.css 50 | 51 | nav: 52 | - Overview: 53 | - Introduction: introduction.md 54 | - Getting Started: getting_started.md 55 | - CLI Usage: cli.md 56 | - Modules: modules.md 57 | - Controllers: controllers.md 58 | - Providers: providers.md 59 | - Guards: guards.md 60 | - Dependency Injection: dependency_injection.md 61 | - Deployment: 62 | - Docker: docker.md 63 | - Application Examples: 64 | - Blank Application: blank.md 65 | - Sync ORM Application: sync_orm.md 66 | - Async ORM Application: async_orm.md 67 | - MongoDB Application: mongodb.md 68 | - License: license.md -------------------------------------------------------------------------------- /nest/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.3.1" 2 | -------------------------------------------------------------------------------- /nest/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/nest/cli/__init__.py -------------------------------------------------------------------------------- /nest/cli/cli.py: -------------------------------------------------------------------------------- 1 | from nest.cli.src.app_module import nest_cli 2 | 3 | if __name__ == "__main__": 4 | nest_cli() 5 | -------------------------------------------------------------------------------- /nest/cli/click_handlers.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import yaml 4 | 5 | from nest.common.templates.templates_factory import TemplateFactory 6 | 7 | 8 | def get_metadata(): 9 | setting_path = Path(__file__).parent.parent / "settings.yaml" 10 | assert setting_path.exists(), "settings.yaml file not found" 11 | with open(setting_path, "r") as file: 12 | file = yaml.load(file, Loader=yaml.FullLoader) 13 | 14 | config = file["config"] 15 | db_type = config["db_type"] 16 | is_async = config["is_async"] 17 | return db_type, is_async 18 | 19 | 20 | def create_nest_app(app_name: str = ".", db_type: str = None, is_async: bool = False): 21 | """ 22 | Create a new nest app 23 | 24 | :param app_name: The name of the app 25 | :param db_type: The type of the database (sqlite, mysql, postgresql) 26 | :param is_async: whether the project should be async or not (only for relational databases) 27 | 28 | The files structure are: 29 | 30 | ├── app_module.py 31 | ├── config.py (only for databases) 32 | ├── main.py 33 | ├── requirements.txt 34 | ├── .gitignore 35 | ├── src 36 | │ ├── __init__.py 37 | 38 | in addition to those files, a setting.yaml file will be created in the package level that will help managed configurations 39 | """ 40 | template_factory = TemplateFactory() 41 | template = template_factory.get_template( 42 | module_name="example", db_type=db_type, is_async=is_async 43 | ) 44 | template.generate_project(app_name) 45 | 46 | 47 | def create_nest_module(name: str): 48 | """ 49 | Create a new nest module 50 | 51 | :param name: The name of the module 52 | 53 | The files structure are: 54 | ├── ... 55 | ├── src 56 | │ ├── __init__.py 57 | │ ├── module_name 58 | ├── __init__.py 59 | ├── module_name_controller.py 60 | ├── module_name_service.py 61 | ├── module_name_model.py 62 | ├── module_name_entity.py (only for databases) 63 | ├── module_name_module.py 64 | """ 65 | db_type, is_async = get_metadata() 66 | template_factory = TemplateFactory() 67 | template = template_factory.get_template( 68 | module_name=name, db_type=db_type, is_async=is_async 69 | ) 70 | template.generate_module(name) 71 | -------------------------------------------------------------------------------- /nest/cli/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/nest/cli/src/__init__.py -------------------------------------------------------------------------------- /nest/cli/src/app_controller.py: -------------------------------------------------------------------------------- 1 | from nest.cli.src.app_service import AppService 2 | from nest.core.decorators.cli.cli_decorators import CliCommand, CliController 3 | 4 | 5 | @CliController("app") 6 | class AppController: 7 | def __init__(self, app_service: AppService): 8 | self.app_service = app_service 9 | 10 | @CliCommand("info") 11 | def get_app_info(self): 12 | app_info = self.app_service.get_app_info() 13 | print(app_info) 14 | 15 | @CliCommand("version", help="Get the version of the app") 16 | def get_app_version(self): 17 | app_info = self.app_service.get_app_info() 18 | print(app_info["app_version"]) 19 | -------------------------------------------------------------------------------- /nest/cli/src/app_module.py: -------------------------------------------------------------------------------- 1 | from nest.cli.src.app_controller import AppController 2 | from nest.cli.src.app_service import AppService 3 | from nest.cli.src.generate.generate_module import GenerateModule 4 | from nest.core import Module 5 | from nest.core.cli_factory import CLIAppFactory 6 | 7 | 8 | @Module( 9 | imports=[GenerateModule], 10 | controllers=[AppController], 11 | providers=[AppService], 12 | ) 13 | class AppModule: 14 | pass 15 | 16 | 17 | nest_cli = CLIAppFactory().create(AppModule) 18 | -------------------------------------------------------------------------------- /nest/cli/src/app_service.py: -------------------------------------------------------------------------------- 1 | from nest import __version__ 2 | from nest.core import Injectable 3 | 4 | 5 | @Injectable 6 | class AppService: 7 | def __init__(self): 8 | self.app_name = "PyNest CLI App" 9 | self.app_version = __version__ 10 | 11 | def get_app_info(self): 12 | return {"app_name": self.app_name, "app_version": self.app_version} 13 | -------------------------------------------------------------------------------- /nest/cli/src/generate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/nest/cli/src/generate/__init__.py -------------------------------------------------------------------------------- /nest/cli/src/generate/generate_controller.py: -------------------------------------------------------------------------------- 1 | import click 2 | from click import Option 3 | 4 | from nest.cli.src.generate.generate_model import SharedOptions 5 | from nest.cli.src.generate.generate_service import GenerateService 6 | from nest.core.decorators.cli.cli_decorators import CliCommand, CliController 7 | 8 | 9 | @CliController("generate") 10 | class GenerateController: 11 | def __init__(self, generate_service: GenerateService): 12 | self.generate_service = generate_service 13 | 14 | @CliCommand("resource") 15 | def generate_resource( 16 | self, 17 | name: SharedOptions.NAME, 18 | path: SharedOptions.PATH, 19 | ): 20 | self.generate_service.generate_resource(name, path) 21 | 22 | @CliCommand("controller", help="Generate a new nest controller") 23 | def generate_controller(self, name: SharedOptions.NAME, path: SharedOptions.PATH): 24 | self.generate_service.generate_controller(name, path) 25 | 26 | @CliCommand("service", help="Generate a new nest service") 27 | def generate_service(self, name: SharedOptions.NAME, path: SharedOptions.PATH): 28 | self.generate_service.generate_service(name, path) 29 | 30 | @CliCommand("module", help="Generate a new nest module") 31 | def generate_module(self, name: SharedOptions.NAME): 32 | self.generate_service.generate_module(name) 33 | 34 | @CliCommand("application", help="Generate a new nest application") 35 | def generate_app( 36 | self, 37 | app_name: SharedOptions.APP_NAME, 38 | db_type: SharedOptions.DB_TYPE, 39 | is_async: SharedOptions.IS_ASYNC, 40 | is_cli: SharedOptions.IS_CLI, 41 | ): 42 | click.echo(f"Generating app {app_name}") 43 | self.generate_service.generate_app(app_name, db_type, is_async, is_cli) 44 | -------------------------------------------------------------------------------- /nest/cli/src/generate/generate_model.py: -------------------------------------------------------------------------------- 1 | import click 2 | from click import Option 3 | 4 | 5 | class SharedOptions: 6 | NAME = Option( 7 | ["-n", "--name"], 8 | help="Name of the resource", 9 | required=True, 10 | type=str, 11 | ) 12 | APP_NAME = Option( 13 | ["-n", "--app-name"], 14 | help="Name of the application", 15 | required=True, 16 | type=str, 17 | ) 18 | PATH = Option( 19 | ["-p", "--path"], help="Path of the resource", required=False, type=str 20 | ) 21 | DB_TYPE = Option( 22 | ["-db", "--db-type"], 23 | help="The type of the database (postgresql, mysql, sqlite, or mongo db).", 24 | required=False, 25 | show_choices=True, 26 | type=str, 27 | default=None, 28 | ) 29 | IS_ASYNC = Option( 30 | ["--is-async"], 31 | help="Whether the project should be async or not (only for relational databases).", 32 | required=False, 33 | is_flag=True, 34 | default=False, 35 | ) 36 | IS_CLI = Option( 37 | ["--is-cli"], 38 | help="Whether the project should be a CLI project or not.", 39 | required=False, 40 | is_flag=True, 41 | default=False, 42 | ) 43 | -------------------------------------------------------------------------------- /nest/cli/src/generate/generate_module.py: -------------------------------------------------------------------------------- 1 | from nest.cli.src.generate.generate_controller import GenerateController 2 | from nest.cli.src.generate.generate_service import GenerateService 3 | from nest.core import Module 4 | 5 | 6 | @Module(controllers=[GenerateController], providers=[GenerateService]) 7 | class GenerateModule: 8 | pass 9 | -------------------------------------------------------------------------------- /nest/cli/src/generate/generate_service.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import click 4 | import yaml 5 | 6 | from nest.cli.templates.templates_factory import TemplateFactory 7 | from nest.core import Injectable 8 | 9 | 10 | @Injectable 11 | class GenerateService: 12 | def __init__(self): 13 | self.template_factory = TemplateFactory() 14 | 15 | @staticmethod 16 | def get_metadata(): 17 | config = {"db_type": None, "is_async": False, "is_cli": False} 18 | setting_path = Path(__file__).parent.parent.parent.parent / "settings.yaml" 19 | if setting_path.exists(): 20 | with open(setting_path, "r") as file: 21 | file = yaml.load(file, Loader=yaml.FullLoader) 22 | config = file["config"] 23 | db_type = config["db_type"] 24 | is_async = config["is_async"] 25 | is_cli = config["is_cli"] if "is_cli" in config else False 26 | return db_type, is_async, is_cli 27 | 28 | def get_template(self, module_name: str): 29 | db_type, is_async, is_cli = self.get_metadata() 30 | template = self.template_factory.get_template( 31 | module_name=module_name, db_type=db_type, is_async=is_async, is_cli=is_cli 32 | ) 33 | return template 34 | 35 | def generate_resource(self, name: str, path: str = None): 36 | """ 37 | Create a new nest resource 38 | 39 | :param name: The name of the module 40 | :param path: The path where the module will be created, Must be in a scope of the src folder. 41 | 42 | The files structure are: 43 | ├── ... 44 | ├── src 45 | │ ├── __init__.py 46 | │ ├── module_name 47 | ├── __init__.py 48 | ├── module_name_controller.py 49 | ├── module_name_service.py 50 | ├── module_name_model.py 51 | ├── module_name_entity.py (only for databases) 52 | ├── module_name_module.py 53 | 54 | """ 55 | if path is None: 56 | path = Path.cwd() 57 | template = self.get_template(name) 58 | template.generate_module(name, path) 59 | 60 | def generate_controller(self, name: str, path: str = None): 61 | """ 62 | Create a new nest controller 63 | 64 | :param name: The name of the controller 65 | 66 | The files structure are: 67 | ├── ... 68 | ├── src 69 | │ ├── __init__.py 70 | │ ├── module_name 71 | ├── __init__.py 72 | ├── module_name_controller.py 73 | """ 74 | template = self.get_template(name) 75 | if path is None: 76 | path = Path.cwd() 77 | with open(f"{path}/{name}_controller.py", "w") as f: 78 | f.write(template.generate_empty_controller_file()) 79 | 80 | def generate_service(self, name: str, path: str = None): 81 | """ 82 | Create a new nest service 83 | 84 | :param name: The name of the service 85 | 86 | The files structure are: 87 | ├── ... 88 | ├── src 89 | │ ├── __init__.py 90 | │ ├── module_name 91 | ├── __init__.py 92 | ├── module_name_service.py 93 | """ 94 | template = self.get_template(name) 95 | if path is None: 96 | path = Path.cwd() 97 | with open(f"{path}/{name}_service.py", "w") as f: 98 | f.write(template.generate_empty_service_file()) 99 | 100 | def generate_module(self, name: str, path: str = None): 101 | """ 102 | Create a new nest module 103 | 104 | :param name: The name of the module 105 | :param path: The path where the module will be created 106 | 107 | The files structure are: 108 | ├── ... 109 | ├── src 110 | │ ├── __init__.py 111 | │ ├── module_name 112 | ├── __init__.py 113 | ├── module_name_module.py 114 | """ 115 | template = self.get_template(name) 116 | if path is None: 117 | path = Path.cwd() / "src" 118 | with open(f"{path}/{name}_module.py", "w") as f: 119 | f.write(template.generate_empty_module_file()) 120 | 121 | def generate_app(self, app_name: str, db_type: str, is_async: bool, is_cli: bool): 122 | """ 123 | Create a new nest app 124 | 125 | :param app_name: The name of the app 126 | :param db_type: The type of the database 127 | :param is_async: Whether the project should be async or not 128 | :param is_cli: Whether the project should be a CLI project or not 129 | 130 | The files structure are: 131 | ├── ... 132 | ├── src 133 | │ ├── __init__.py 134 | │ ├── app_name 135 | ├── __init__.py 136 | ├── app_name_controller.py 137 | ├── app_name_service.py 138 | ├── app_name_model.py 139 | ├── app_name_entity.py (only for databases) 140 | ├── app_name_module.py 141 | """ 142 | template = self.template_factory.get_template( 143 | module_name=app_name, db_type=db_type, is_async=is_async, is_cli=is_cli 144 | ) 145 | template.generate_project(app_name) 146 | -------------------------------------------------------------------------------- /nest/cli/templates/__init__.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Database(Enum): 5 | POSTGRESQL = "postgresql" 6 | MYSQL = "mysql" 7 | SQLITE = "sqlite" 8 | MONGODB = "mongodb" 9 | CLI = "cli" 10 | 11 | def __str__(self): 12 | return self.value 13 | -------------------------------------------------------------------------------- /nest/cli/templates/abstract_empty_template.py: -------------------------------------------------------------------------------- 1 | from nest.core.templates.abstract_base_template import AbstractBaseTemplate 2 | 3 | 4 | class AbstractEmptyTemplate(AbstractBaseTemplate): 5 | def __init__(self): 6 | super().__init__(is_empty=True) 7 | 8 | def generate_app_file(self) -> str: 9 | return f"""from nest.core.app import App 10 | from src.examples.examples_module import ExamplesModule 11 | 12 | app = App( 13 | description="Blank PyNest service", 14 | modules=[ 15 | ExamplesModule, 16 | ] 17 | ) 18 | """ 19 | 20 | def generate_controller_file(self, name) -> str: 21 | return f"""from nest.core import Controller, Get, Post, Depends, Delete, Put 22 | 23 | from src.{name}.{name}_service import {self.capitalized_name}Service 24 | from src.{name}.{name}_model import {self.capitalized_name} 25 | 26 | 27 | @Controller("{name}", tag="{name}") 28 | class {self.capitalized_name}Controller: 29 | 30 | service: {self.capitalized_name}Service = Depends({self.capitalized_name}Service) 31 | 32 | @Get("/get_{name}") 33 | {self.is_async}def get_{name}(self): 34 | return {self.is_await}self.service.get_{name}() 35 | """ 36 | -------------------------------------------------------------------------------- /nest/cli/templates/blank_template.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from pathlib import Path 3 | 4 | from nest.cli.templates.base_template import BaseTemplate 5 | 6 | 7 | class BlankTemplate(BaseTemplate, ABC): 8 | def __init__(self, module_name: str): 9 | super().__init__(module_name) 10 | 11 | def app_file(self): 12 | return f"""from nest.core import PyNestFactory, Module 13 | 14 | from .app_controller import AppController 15 | from .app_service import AppService 16 | 17 | 18 | @Module(imports=[], controllers=[AppController], providers=[AppService]) 19 | class AppModule: 20 | pass 21 | 22 | 23 | app = PyNestFactory.create( 24 | AppModule, 25 | description="This is my PyNest app.", 26 | title="PyNest Application", 27 | version="1.0.0", 28 | debug=True, 29 | ) 30 | 31 | http_server = app.get_server() 32 | 33 | """ 34 | 35 | def config_file(self): 36 | pass 37 | 38 | def docker_file(self): 39 | pass 40 | 41 | def dockerignore_file(self): 42 | pass 43 | 44 | def gitignore_file(self): 45 | pass 46 | 47 | def model_file(self): 48 | return f"""from pydantic import BaseModel 49 | 50 | 51 | class {self.capitalized_module_name}(BaseModel): 52 | name: str 53 | 54 | """ 55 | 56 | def service_file(self): 57 | return f"""from .{self.module_name}_model import {self.capitalized_module_name} 58 | from nest.core import Injectable 59 | 60 | 61 | @Injectable 62 | class {self.capitalized_module_name}Service: 63 | 64 | def __init__(self): 65 | self.database = [] 66 | 67 | def get_{self.module_name}(self): 68 | return self.database 69 | 70 | def add_{self.module_name}(self, {self.module_name}: {self.capitalized_module_name}): 71 | self.database.append({self.module_name}) 72 | return {self.module_name} 73 | """ 74 | 75 | def controller_file(self): 76 | return f"""from nest.core import Controller, Get, Post 77 | from .{self.module_name}_service import {self.capitalized_module_name}Service 78 | from .{self.module_name}_model import {self.capitalized_module_name} 79 | 80 | 81 | @Controller("{self.module_name}", tag="{self.module_name}") 82 | class {self.capitalized_module_name}Controller: 83 | 84 | def __init__(self, {self.module_name}_service: {self.capitalized_module_name}Service): 85 | self.{self.module_name}_service = {self.module_name}_service 86 | 87 | @Get("/") 88 | def get_{self.module_name}(self): 89 | return self.{self.module_name}_service.get_{self.module_name}() 90 | 91 | @Post("/") 92 | def add_{self.module_name}(self, {self.module_name}: {self.capitalized_module_name}): 93 | return self.{self.module_name}_service.add_{self.module_name}({self.module_name}) 94 | 95 | """ 96 | 97 | def entity_file(self): 98 | pass 99 | 100 | def create_module(self, module_name: str, src_path: Path): 101 | module_path = src_path / module_name 102 | self.create_folder(module_path) 103 | self.create_template(module_path / "__init__.py", "") 104 | self.create_template( 105 | module_path / f"{module_name}_module.py", self.module_file() 106 | ) 107 | self.create_template( 108 | module_path / f"{module_name}_controller.py", self.controller_file() 109 | ) 110 | self.create_template( 111 | module_path / f"{module_name}_service.py", self.service_file() 112 | ) 113 | self.create_template(module_path / f"{module_name}_model.py", self.model_file()) 114 | self.append_module_to_app(path_to_app_py=f"{src_path / 'app_module.py'}") 115 | 116 | def generate_module(self, module_name: str, path: str = None): 117 | src_path = self.validate_new_module(module_name) 118 | self.create_module(module_name, src_path) 119 | 120 | def generate_project(self, project_name: str): 121 | self.create_template(self.nest_path / "settings.yaml", self.settings_file()) 122 | root = self.base_path / project_name 123 | src_path = root / "src" 124 | self.create_folder(root) 125 | self.create_template(root / "main.py", self.main_file()) 126 | self.create_template(root / "README.md", self.readme_file()) 127 | self.create_template(root / "requirements.txt", self.requirements_file()) 128 | self.create_folder(src_path) 129 | self.create_template(src_path / "__init__.py", "") 130 | self.create_template(src_path / "app_module.py", self.app_file()) 131 | self.create_template(src_path / "app_controller.py", self.app_controller_file()) 132 | self.create_template(src_path / "app_service.py", self.app_service_file()) 133 | 134 | def settings_file(self): 135 | return f"""# This file is used to configure the nest cli. 136 | config: 137 | db_type: null 138 | is_async: false 139 | """ 140 | 141 | def requirements_file(self): 142 | return f"""pynest-api""" 143 | -------------------------------------------------------------------------------- /nest/cli/templates/cli_templates.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from nest.cli.templates.base_template import BaseTemplate 4 | from nest.core.decorators.cli.cli_decorators import CliCommand, CliController 5 | 6 | 7 | class ClickTemplate(BaseTemplate): 8 | def __init__(self, module_name: str): 9 | super().__init__(module_name) 10 | 11 | def app_file(self): 12 | return f"""from nest.core.pynest_factory import PyNestFactory 13 | from nest.core.cli_factory import CLIAppFactory 14 | from nest.core import Module 15 | from src.app_service import AppService 16 | from src.app_controller import AppController 17 | 18 | 19 | @Module( 20 | imports=[], 21 | controllers=[AppController], 22 | providers=[AppService], 23 | ) 24 | class AppModule: 25 | pass 26 | 27 | 28 | cli_app = CLIAppFactory().create(AppModule) 29 | 30 | if __name__ == "__main__": 31 | cli_app() 32 | """ 33 | 34 | def module_file(self): 35 | return f"""from nest.core import Module 36 | from src.{self.module_name}.{self.module_name}_controller import {self.module_name.capitalize()}Controller 37 | from src.{self.module_name}.{self.module_name}_service import {self.module_name.capitalize()}Service 38 | 39 | 40 | @Module( 41 | controllers=[{self.module_name.capitalize()}Controller], 42 | providers=[{self.module_name.capitalize()}Service], 43 | ) 44 | class {self.module_name.capitalize()}Module: 45 | pass 46 | """ 47 | 48 | def controller_file(self): 49 | return f"""from nest.core.decorators.cli.cli_decorators import CliCommand, CliController 50 | from src.{self.module_name}.{self.module_name}_service import {self.module_name.capitalize()}Service 51 | 52 | @CliController("{self.module_name}") 53 | class {self.module_name.capitalize()}Controller: 54 | def __init__(self, {self.module_name}_service: {self.module_name.capitalize()}Service): 55 | self.{self.module_name}_service = {self.module_name}_service 56 | 57 | @CliCommand("hello") 58 | def hello(self): 59 | self.{self.module_name}_service.hello() 60 | """ 61 | 62 | def service_file(self): 63 | return f"""from nest.core import Injectable 64 | 65 | @Injectable 66 | class {self.module_name.capitalize()}Service: 67 | 68 | 69 | def hello(self): 70 | print("Hello from {self.module_name.capitalize()}Service") 71 | """ 72 | 73 | def settings_file(self): 74 | return f"""config: 75 | db_type: '' 76 | is_async: False 77 | is_cli: True 78 | """ 79 | 80 | def app_controller_file(self): 81 | return f"""from nest.core.decorators.cli.cli_decorators import CliCommand, CliController 82 | from src.app_service import AppService 83 | 84 | 85 | @CliController("app") 86 | class AppController: 87 | def __init__(self, app_service: AppService): 88 | self.app_service = app_service 89 | 90 | @CliCommand("version") 91 | def version(self): 92 | self.app_service.version() 93 | 94 | @CliCommand("info") 95 | def info(self): 96 | self.app_service.info() 97 | """ 98 | 99 | def app_service_file(self): 100 | return f"""from nest.core import Injectable 101 | import click 102 | 103 | 104 | @Injectable 105 | class AppService: 106 | 107 | def version(self): 108 | print(click.style("1.0.0", fg="blue")) 109 | 110 | def info(self): 111 | print(click.style("This is a cli nest app!", fg="green")) 112 | """ 113 | 114 | def create_module(self, module_name: str, src_path: Path): 115 | module_path = src_path / module_name 116 | self.create_folder(module_path) 117 | self.create_template(module_path / "__init__.py", "") 118 | self.create_template( 119 | module_path / f"{module_name}_module.py", self.module_file() 120 | ) 121 | self.create_template( 122 | module_path / f"{module_name}_controller.py", self.controller_file() 123 | ) 124 | self.create_template( 125 | module_path / f"{module_name}_service.py", self.service_file() 126 | ) 127 | self.append_module_to_app(path_to_app_py=f"{src_path / 'app_module.py'}") 128 | 129 | def generate_module(self, module_name: str, path: str = None): 130 | src_path = self.validate_new_module(module_name) 131 | self.create_module(module_name, src_path) 132 | 133 | def generate_project(self, project_name: str): 134 | self.create_template(self.nest_path / "settings.yaml", self.settings_file()) 135 | root = self.base_path / project_name 136 | src_path = root / "src" 137 | self.create_folder(root) 138 | self.create_template(root / "README.md", self.readme_file()) 139 | self.create_template(root / "requirements.txt", self.requirements_file()) 140 | self.create_folder(src_path) 141 | self.create_template(src_path / "__init__.py", "") 142 | self.create_template(src_path / "app_module.py", self.app_file()) 143 | self.create_template(src_path / "app_controller.py", self.app_controller_file()) 144 | self.create_template(src_path / "app_service.py", self.app_service_file()) 145 | 146 | def requirements_file(self): 147 | return f"""pynest-api""" 148 | 149 | def config_file(self): 150 | return "" 151 | 152 | def docker_file(self): 153 | return "" 154 | 155 | def dockerignore_file(self): 156 | return "" 157 | 158 | def entity_file(self): 159 | return "" 160 | 161 | def gitignore_file(self): 162 | return "" 163 | 164 | def model_file(self): 165 | return "" 166 | -------------------------------------------------------------------------------- /nest/cli/templates/mongo_db_template.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from nest.cli.templates.abstract_base_template import AbstractBaseTemplate 4 | 5 | 6 | class MongoDbTemplate(AbstractBaseTemplate, ABC): 7 | def __init__(self, name: str): 8 | self.name = name 9 | self.db_type = "mongodb" 10 | super().__init__(self.name, self.db_type) 11 | 12 | def generate_service_file(self) -> str: 13 | return f"""from src.{self.name}.{self.name}_model import {self.capitalized_name} 14 | from src.{self.name}.{self.name}_entity import {self.capitalized_name} as {self.capitalized_name}Entity 15 | from nest.core.decorators import db_request_handler 16 | from functools import lru_cache 17 | 18 | 19 | @lru_cache() 20 | class {self.capitalized_name}Service: 21 | 22 | @db_request_handler 23 | async def get_{self.name}(self): 24 | return await {self.capitalized_name}Entity.find_all().to_list() 25 | 26 | @db_request_handler 27 | async def add_{self.name}(self, {self.name}: {self.capitalized_name}): 28 | new_{self.name} = {self.capitalized_name}Entity( 29 | **{self.name}.dict() 30 | ) 31 | await new_{self.name}.save() 32 | return new_{self.name}.id 33 | 34 | 35 | @db_request_handler 36 | async def update_{self.name}(self, {self.name}: {self.capitalized_name}): 37 | return await {self.capitalized_name}Entity.find_one_and_update( 38 | {{"id": {self.name}.id}}, {self.name}.dict() 39 | ) 40 | 41 | @db_request_handler 42 | async def delete_{self.name}(self, {self.name}: {self.capitalized_name}): 43 | return await {self.capitalized_name}Entity.find_one_and_delete( 44 | {{"id": {self.name}.id}} 45 | ) 46 | """ 47 | 48 | def generate_orm_config_file(self) -> str: 49 | return f"""from nest.core.database.base_odm import OdmService 50 | from src.examples.examples_entity import Examples 51 | import os 52 | from dotenv import load_dotenv 53 | 54 | load_dotenv() 55 | 56 | config = OdmService( 57 | db_type="{self.db_type}", 58 | config_params={{ 59 | "db_name": os.getenv("DB_NAME"), 60 | "host": os.getenv("DB_HOST"), 61 | "user": os.getenv("DB_USER"), 62 | "password": os.getenv("DB_PASSWORD"), 63 | "port": os.getenv("DB_PORT"), 64 | }}, 65 | document_models=[Examples] 66 | ) 67 | """ 68 | 69 | def generate_entity_file(self) -> str: 70 | return f"""from beanie import Document 71 | 72 | 73 | class {self.capitalized_name}(Document): 74 | name: str 75 | 76 | class Config: 77 | schema_extra = {{ 78 | "example": {{ 79 | "name": "Example Name", 80 | }} 81 | }} 82 | """ 83 | 84 | def generate_requirements_file(self) -> str: 85 | return f""" 86 | pynest-api 87 | """ 88 | 89 | def generate_dockerfile(self) -> str: 90 | pass 91 | -------------------------------------------------------------------------------- /nest/cli/templates/mongo_template.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from abc import ABC 3 | from pathlib import Path 4 | 5 | from nest.cli.templates import Database 6 | from nest.cli.templates.orm_template import AsyncORMTemplate 7 | 8 | 9 | class MongoTemplate(AsyncORMTemplate, ABC): 10 | def __init__(self, module_name: str): 11 | super().__init__( 12 | module_name=module_name, 13 | db_type=Database.MONGODB, 14 | ) 15 | 16 | def config_file(self): 17 | return f"""import os 18 | from dotenv import load_dotenv 19 | from nest.core.database.odm_provider import OdmProvider 20 | 21 | load_dotenv() 22 | 23 | config = OdmProvider( 24 | config_params={{ 25 | "db_name": os.getenv("DB_NAME", "default_nest_db"), 26 | "host": os.getenv("DB_HOST", "localhost"), 27 | "user": os.getenv("DB_USER", "root"), 28 | "password": os.getenv("DB_PASSWORD", "root"), 29 | "port": os.getenv("DB_PORT", 27017), 30 | }}, 31 | document_models=[] 32 | ) 33 | """ 34 | 35 | def requirements_file(self): 36 | return f"""pynest-api 37 | beanie==1.20.0""" 38 | 39 | def docker_file(self): 40 | return "" 41 | 42 | def entity_file(self): 43 | return f"""from beanie import Document 44 | 45 | 46 | class {self.capitalized_module_name}(Document): 47 | name: str 48 | 49 | class Config: 50 | schema_extra = {{ 51 | "example": {{ 52 | "name": "Example Name", 53 | }} 54 | }} 55 | """ 56 | 57 | def controller_file(self): 58 | return f"""from nest.core import Controller, Get, Post 59 | 60 | from .{self.module_name}_service import {self.capitalized_module_name}Service 61 | from .{self.module_name}_model import {self.capitalized_module_name} 62 | 63 | 64 | @Controller("{self.module_name}", tag="{self.module_name}") 65 | class {self.capitalized_module_name}Controller: 66 | 67 | def __init__(self, {self.module_name}_service: {self.capitalized_module_name}Service): 68 | self.service = service 69 | 70 | @Get("/") 71 | async def get_{self.module_name}(self): 72 | return await self.{self.module_name}_service.get_{self.module_name}() 73 | 74 | @Post("/") 75 | async def add_{self.module_name}(self, {self.module_name}: {self.capitalized_module_name}): 76 | return await self.{self.module_name}_service.add_{self.module_name}({self.module_name}) 77 | """ 78 | 79 | def service_file(self): 80 | return f"""from .{self.module_name}_model import {self.capitalized_module_name} 81 | from .{self.module_name}_entity import {self.capitalized_module_name} as {self.capitalized_module_name}Entity 82 | from nest.core.decorators.database import db_request_handler 83 | from nest.core import Injectable 84 | 85 | 86 | @Injectable 87 | class {self.capitalized_module_name}Service: 88 | 89 | @db_request_handler 90 | async def add_{self.module_name}(self, {self.module_name}: {self.capitalized_module_name}): 91 | new_{self.module_name} = {self.capitalized_module_name}Entity( 92 | **{self.module_name}.dict() 93 | ) 94 | await new_{self.module_name}.save() 95 | return new_{self.module_name}.id 96 | 97 | @db_request_handler 98 | async def get_{self.module_name}(self): 99 | return await {self.capitalized_module_name}Entity.find_all().to_list() 100 | """ 101 | 102 | def add_document_to_odm_config(self, config_file: Path): 103 | tree = self.append_import( 104 | file_path=config_file, 105 | module_path=f"src.{self.module_name}.{self.module_name}_entity", 106 | class_name=self.capitalized_module_name, 107 | import_exception="from nest.core.database.odm_provider import OdmProvider", 108 | ) 109 | modified = False 110 | 111 | for node in ast.walk(tree): 112 | if ( 113 | isinstance(node, ast.Call) 114 | and hasattr(node.func, "id") 115 | and node.func.id == "OdmProvider" 116 | ): 117 | for keyword in node.keywords: 118 | if keyword.arg == "document_models": 119 | if isinstance(keyword.value, ast.List): 120 | # Append to existing list 121 | keyword.value.elts.append( 122 | ast.Name( 123 | id=self.capitalized_module_name, ctx=ast.Load() 124 | ) 125 | ) 126 | modified = True 127 | break 128 | 129 | if modified: 130 | self.save_file_with_astor(config_file, tree) 131 | self.format_with_black(config_file) 132 | 133 | def generate_module(self, module_name: str, path: str = None): 134 | src_path = self.validate_new_module(module_name) 135 | config_file = self.validate_config_file(src_path) 136 | self.create_module( 137 | src_path=src_path, 138 | module_name=module_name, 139 | ) 140 | self.add_document_to_odm_config(config_file) 141 | -------------------------------------------------------------------------------- /nest/cli/templates/mysql_template.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from nest.cli.templates import Database 4 | from nest.cli.templates.orm_template import AsyncORMTemplate, ORMTemplate 5 | 6 | 7 | class MySQLTemplate(ORMTemplate, ABC): 8 | def __init__(self, module_name: str): 9 | super().__init__( 10 | module_name=module_name, 11 | db_type=Database.MYSQL, 12 | ) 13 | 14 | def config_file(self): 15 | return """from nest.core.database.orm_provider import OrmProvider 16 | import os 17 | from dotenv import load_dotenv 18 | 19 | load_dotenv() 20 | 21 | config = OrmProvider( 22 | db_type="mysql", 23 | config_params=dict( 24 | host=os.getenv("MYSQL_HOST"), 25 | db_name=os.getenv("MYSQL_DB_NAME"), 26 | user=os.getenv("MYSQL_USER"), 27 | password=os.getenv("MYSQL_PASSWORD"), 28 | port=int(os.getenv("MYSQL_PORT")), 29 | ) 30 | ) 31 | """ 32 | 33 | def requirements_file(self): 34 | return f"""pynest-api 35 | mysql-connector-python==8.2.0 36 | """ 37 | 38 | 39 | class AsyncMySQLTemplate(AsyncORMTemplate, ABC): 40 | def __init__(self, module_name: str): 41 | super().__init__( 42 | module_name=module_name, 43 | db_type=Database.MYSQL, 44 | ) 45 | 46 | def config_file(self): 47 | return """from nest.core.database.orm_provider import AsyncOrmProvider 48 | import os 49 | from dotenv import load_dotenv 50 | 51 | load_dotenv() 52 | 53 | config = AsyncOrmProvider( 54 | db_type="mysql", 55 | config_params=dict( 56 | host=os.getenv("MYSQL_HOST"), 57 | db_name=os.getenv("MYSQL_DB_NAME"), 58 | user=os.getenv("MYSQL_USER"), 59 | password=os.getenv("MYSQL_PASSWORD"), 60 | port=int(os.getenv("MYSQL_PORT")), 61 | ) 62 | ) 63 | """ 64 | 65 | def requirements_file(self): 66 | return f"""pynest-api 67 | aiomysql==0.2.0 68 | """ 69 | -------------------------------------------------------------------------------- /nest/cli/templates/postgres_template.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from nest.cli.templates import Database 4 | from nest.cli.templates.orm_template import AsyncORMTemplate, ORMTemplate 5 | 6 | 7 | class PostgresqlTemplate(ORMTemplate, ABC): 8 | def __init__(self, module_name: str): 9 | super().__init__( 10 | module_name=module_name, 11 | db_type=Database.POSTGRESQL, 12 | ) 13 | 14 | def config_file(self): 15 | return """from nest.core.database.orm_provider import OrmProvider 16 | import os 17 | from dotenv import load_dotenv 18 | 19 | load_dotenv() 20 | 21 | config = OrmProvider( 22 | db_type="postgresql", 23 | config_params=dict( 24 | host=os.getenv("POSTGRESQL_HOST", "localhost"), 25 | db_name=os.getenv("POSTGRESQL_DB_NAME", "default_nest_db"), 26 | user=os.getenv("POSTGRESQL_USER", "postgres"), 27 | password=os.getenv("POSTGRESQL_PASSWORD", "postgres"), 28 | port=int(os.getenv("POSTGRESQL_PORT", 5432)), 29 | ) 30 | ) 31 | """ 32 | 33 | def requirements_file(self): 34 | return f"""pynest-api 35 | psycopg2==2.9.6 36 | """ 37 | 38 | 39 | class AsyncPostgresqlTemplate(AsyncORMTemplate, ABC): 40 | def __init__(self, module_name: str): 41 | super().__init__( 42 | module_name=module_name, 43 | db_type=Database.POSTGRESQL, 44 | ) 45 | 46 | def config_file(self): 47 | return """from nest.core.database.orm_provider import AsyncOrmProvider 48 | import os 49 | from dotenv import load_dotenv 50 | 51 | load_dotenv() 52 | 53 | config = AsyncOrmProvider( 54 | db_type="postgresql", 55 | config_params=dict( 56 | host=os.getenv("POSTGRESQL_HOST", "localhost"), 57 | db_name=os.getenv("POSTGRESQL_DB_NAME", "default_nest_db"), 58 | user=os.getenv("POSTGRESQL_USER", "postgres"), 59 | password=os.getenv("POSTGRESQL_PASSWORD", "postgres"), 60 | port=int(os.getenv("POSTGRESQL_PORT", 5432)), 61 | ) 62 | ) 63 | """ 64 | 65 | def requirements_file(self): 66 | return f"""pynest-api 67 | asyncpg==0.29.0 68 | """ 69 | -------------------------------------------------------------------------------- /nest/cli/templates/relational_db_template.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from nest import __version__ as version 4 | from nest.cli.templates.abstract_base_template import AbstractBaseTemplate 5 | 6 | 7 | class RelationalDBTemplate(AbstractBaseTemplate, ABC): 8 | def __init__(self, name, db_type): 9 | super().__init__(name, db_type) 10 | 11 | def generate_service_file(self) -> str: 12 | return f"""from src.{self.name}.{self.name}_model import {self.capitalized_name} 13 | from src.{self.name}.{self.name}_entity import {self.capitalized_name} as {self.capitalized_name}Entity 14 | from orm_config import config 15 | from nest.core.decorators import db_request_handler 16 | from functools import lru_cache 17 | 18 | 19 | @lru_cache() 20 | class {self.capitalized_name}Service: 21 | 22 | def __init__(self): 23 | self.orm_config = config 24 | self.session = self.orm_config.get_db() 25 | 26 | @db_request_handler 27 | def add_{self.name}(self, {self.name}: {self.capitalized_name}): 28 | new_{self.name} = {self.capitalized_name}Entity( 29 | **{self.name}.dict() 30 | ) 31 | self.session.add(new_{self.name}) 32 | self.session.commit() 33 | return new_{self.name}.id 34 | 35 | @db_request_handler 36 | def get_{self.name}(self): 37 | return self.session.query({self.capitalized_name}Entity).all() 38 | 39 | @db_request_handler 40 | def delete_{self.name}(self, {self.name}_id: int): 41 | self.session.query({self.capitalized_name}Entity).filter_by(id={self.name}_id).delete() 42 | self.session.commit() 43 | return {self.name}_id 44 | 45 | @db_request_handler 46 | def update_{self.name}(self, {self.name}_id: int, {self.name}: {self.capitalized_name}): 47 | self.session.query({self.capitalized_name}Entity).filter_by(id={self.name}_id).update( 48 | {self.name}.dict() 49 | ) 50 | self.session.commit() 51 | return {self.name}_id 52 | """ 53 | 54 | def generate_entity_file(self) -> str: 55 | return f"""from orm_config import config 56 | from sqlalchemy import Column, Integer, String, Float 57 | 58 | 59 | class {self.capitalized_name}(config.Base): 60 | __tablename__ = "{self.name}" 61 | 62 | id = Column(Integer, primary_key=True, autoincrement=True) 63 | name = Column(String, unique=True) 64 | """ 65 | 66 | def generate_requirements_file(self) -> str: 67 | return f"""anyio==3.6.2 68 | click==8.1.3 69 | fastapi==0.95.1 70 | fastapi-utils==0.2.1 71 | greenlet==2.0.2 72 | h11==0.14.0 73 | idna==3.4 74 | pydantic==1.10.7 75 | python-dotenv==1.0.0 76 | sniffio==1.3.0 77 | SQLAlchemy==1.4.48 78 | starlette==0.26.1 79 | typing_extensions==4.5.0 80 | uvicorn==0.22.0 81 | pynest-api=={version} 82 | """ 83 | 84 | def generate_dockerfile(self) -> str: 85 | pass 86 | 87 | def generate_orm_config_file(self) -> str: 88 | base_template = f"""from nest.core.database.base_orm import OrmService 89 | import os 90 | from dotenv import load_dotenv 91 | 92 | load_dotenv() 93 | 94 | 95 | """ 96 | 97 | if self.db_type == "sqlite": 98 | return f"""{base_template} 99 | config = OrmService( 100 | db_type="{self.db_type}", 101 | config_params=dict( 102 | db_name=os.getenv("SQLITE_DB_NAME", "{self.name}_db"), 103 | ) 104 | ) 105 | """ 106 | else: 107 | return f"""{base_template} 108 | config = OrmService( 109 | db_type="{self.db_type}", 110 | config_params=dict( 111 | host=os.getenv("{self.db_type.upper()}_HOST"), 112 | db_name=os.getenv("{self.db_type.upper()}_DB_NAME"), 113 | user=os.getenv("{self.db_type.upper()}_USER"), 114 | password=os.getenv("{self.db_type.upper()}_PASSWORD"), 115 | port=int(os.getenv("{self.db_type.upper()}_PORT")), 116 | ) 117 | ) 118 | """ 119 | 120 | 121 | if __name__ == "__main__": 122 | relational_db_template = RelationalDBTemplate("users", "mysql") 123 | print(relational_db_template.generate_orm_config_file()) 124 | -------------------------------------------------------------------------------- /nest/cli/templates/sqlite_template.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from nest.cli.templates import Database 4 | from nest.cli.templates.orm_template import AsyncORMTemplate, ORMTemplate 5 | 6 | 7 | class SQLiteTemplate(ORMTemplate, ABC): 8 | def __init__(self, module_name: str): 9 | super().__init__( 10 | module_name=module_name, 11 | db_type=Database.SQLITE, 12 | ) 13 | 14 | def config_file(self): 15 | return """from nest.core.database.orm_provider import OrmProvider 16 | import os 17 | from dotenv import load_dotenv 18 | 19 | load_dotenv() 20 | 21 | config = OrmProvider( 22 | db_type="sqlite", 23 | config_params=dict( 24 | db_name=os.getenv("SQLITE_DB_NAME", "default_nest_db"), 25 | ) 26 | ) 27 | """ 28 | 29 | def requirements_file(self): 30 | return f"""pynest-api""" 31 | 32 | def docker_file(self): 33 | return """FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11 34 | 35 | COPY ./app /app/app 36 | COPY ./requirements.txt /app/requirements.txt 37 | """ 38 | 39 | 40 | class AsyncSQLiteTemplate(AsyncORMTemplate, ABC): 41 | def __init__(self, module_name: str): 42 | super().__init__( 43 | module_name=module_name, 44 | db_type=Database.SQLITE, 45 | ) 46 | 47 | def config_file(self): 48 | return """from nest.core.database.orm_provider import AsyncOrmProvider 49 | import os 50 | from dotenv import load_dotenv 51 | 52 | load_dotenv() 53 | 54 | config = AsyncOrmProvider( 55 | db_type="sqlite", 56 | config_params=dict( 57 | db_name=os.getenv("SQLITE_DB_NAME", "default_nest_db"), 58 | ) 59 | ) 60 | """ 61 | 62 | def requirements_file(self): 63 | return f"""pynest-api 64 | aiosqlite==0.19.0""" 65 | 66 | def docker_file(self): 67 | return """FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11 68 | 69 | COPY ./app /app/app 70 | COPY ./requirements.txt /app/requirements.txt 71 | """ 72 | -------------------------------------------------------------------------------- /nest/cli/templates/templates_factory.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | from nest.cli.templates import Database 4 | from nest.cli.templates.base_template import BaseTemplate 5 | from nest.cli.templates.blank_template import BlankTemplate 6 | from nest.cli.templates.cli_templates import ClickTemplate 7 | from nest.cli.templates.mongo_template import MongoTemplate 8 | from nest.cli.templates.mysql_template import AsyncMySQLTemplate, MySQLTemplate 9 | from nest.cli.templates.postgres_template import ( 10 | AsyncPostgresqlTemplate, 11 | PostgresqlTemplate, 12 | ) 13 | from nest.cli.templates.sqlite_template import AsyncSQLiteTemplate, SQLiteTemplate 14 | 15 | 16 | class TemplateFactory: 17 | @staticmethod 18 | def get_template( 19 | db_type: Union[Database, str, None], 20 | module_name: str, 21 | is_async: Optional[bool] = False, 22 | is_cli: Optional[bool] = False, 23 | ) -> BaseTemplate: 24 | if is_cli: 25 | return ClickTemplate(module_name=module_name) 26 | if not db_type: 27 | return BlankTemplate(module_name=module_name) 28 | elif db_type == Database.POSTGRESQL.value: 29 | if is_async: 30 | return AsyncPostgresqlTemplate(module_name=module_name) 31 | else: 32 | return PostgresqlTemplate(module_name=module_name) 33 | elif db_type == Database.MYSQL.value: 34 | if is_async: 35 | return AsyncMySQLTemplate(module_name=module_name) 36 | else: 37 | return MySQLTemplate(module_name=module_name) 38 | elif db_type == Database.SQLITE.value: 39 | if is_async: 40 | return AsyncSQLiteTemplate(module_name=module_name) 41 | else: 42 | return SQLiteTemplate(module_name=module_name) 43 | elif db_type == Database.MONGODB.value: 44 | return MongoTemplate(module_name=module_name) 45 | else: 46 | raise ValueError(f"Unknown database type: {db_type}") 47 | -------------------------------------------------------------------------------- /nest/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/nest/common/__init__.py -------------------------------------------------------------------------------- /nest/common/constants.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ModuleMetadata(str, Enum): 5 | CONTROLLERS = "controllers" 6 | IMPORTS = "imports" 7 | PROVIDERS = "providers" 8 | EXPORTS = "exports" 9 | 10 | def __str__(self): 11 | return self.value 12 | 13 | 14 | INJECTABLE_TOKEN = "__injectable__" 15 | INJECTABLE_NAME = "__injectable__name__" 16 | STATUS_CODE_TOKEN = "status_code" 17 | DEPENDENCIES = "__dependencies__" 18 | -------------------------------------------------------------------------------- /nest/common/exceptions.py: -------------------------------------------------------------------------------- 1 | class CircularDependencyException(Exception): 2 | def __init__(self, message="Circular dependency detected"): 3 | super().__init__(message) 4 | 5 | 6 | class UnknownModuleException(Exception): 7 | pass 8 | 9 | 10 | class NoneInjectableException(Exception): 11 | def __init__(self, message="None Injectable Classe Detected"): 12 | super().__init__(message) 13 | -------------------------------------------------------------------------------- /nest/common/route_resolver.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, FastAPI 2 | 3 | 4 | class RoutesResolver: 5 | def __init__(self, container, app_ref: FastAPI): 6 | self.container = container 7 | self.app_ref = app_ref 8 | 9 | def register_routes(self): 10 | for module in self.container.modules.values(): 11 | for controller in module.controllers.values(): 12 | self.register_route(controller) 13 | 14 | def register_route(self, controller): 15 | router: APIRouter = controller.get_router() 16 | self.app_ref.include_router(router) 17 | -------------------------------------------------------------------------------- /nest/core/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends 2 | 3 | from nest.core.decorators import ( 4 | Controller, 5 | Delete, 6 | Get, 7 | HttpCode, 8 | Injectable, 9 | Module, 10 | Patch, 11 | Post, 12 | Put, 13 | ) 14 | from nest.core.decorators.guards import BaseGuard, UseGuards 15 | from nest.core.pynest_application import PyNestApp 16 | from nest.core.pynest_container import PyNestContainer 17 | from nest.core.pynest_factory import PyNestFactory 18 | -------------------------------------------------------------------------------- /nest/core/cli_factory.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import click 4 | 5 | from nest.core.pynest_container import PyNestContainer 6 | from nest.core.pynest_factory import AbstractPyNestFactory, ModuleType 7 | 8 | 9 | class CLIAppFactory(AbstractPyNestFactory): 10 | def __init__(self): 11 | super().__init__() 12 | 13 | def create(self, app_module: ModuleType, **kwargs): 14 | container = PyNestContainer() 15 | container.add_module(app_module) 16 | 17 | cli_app = click.Group("main") 18 | for module in container.modules.values(): 19 | for controller in module.controllers.values(): 20 | for command in controller._cli_group.commands.values(): 21 | original_callback = command.callback 22 | if asyncio.iscoroutinefunction(original_callback): 23 | command.callback = self._run_async(original_callback) 24 | cli_app.add_command(controller._cli_group) 25 | return cli_app 26 | 27 | @staticmethod 28 | def _run_async(coro): 29 | def wrapper(*args, **kwargs): 30 | return asyncio.run(coro(*args, **kwargs)) 31 | 32 | return wrapper 33 | -------------------------------------------------------------------------------- /nest/core/database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/nest/core/database/__init__.py -------------------------------------------------------------------------------- /nest/core/database/base_config.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | 4 | class ConfigFactoryBase: 5 | """ 6 | Factory class for retrieving the appropriate ORM configuration based on the database type. 7 | 8 | Args: 9 | db_type (str): The type of database. 10 | 11 | """ 12 | 13 | def __init__(self, db_type: str): 14 | """ 15 | Initializes the ConfigFactory instance. 16 | 17 | Args: 18 | db_type (str): The type of database. 19 | 20 | """ 21 | self.db_type = db_type 22 | 23 | def get_config(self): 24 | """ 25 | Returns the appropriate ORM configuration class based on the database type. 26 | 27 | Returns: 28 | class: The ORM configuration class. 29 | 30 | Raises: 31 | Exception: If the database type is not supported. 32 | 33 | """ 34 | assert self.db_type, "db_type is required" 35 | 36 | 37 | class BaseConfig: 38 | """ 39 | Base abstract class for ODM (Object-Document Mapping) configurations. 40 | 41 | """ 42 | 43 | @abstractmethod 44 | def get_engine_url(self) -> str: 45 | """ 46 | Returns the engine URL for the ORM. 47 | 48 | Returns: 49 | str: The engine URL. 50 | 51 | """ 52 | pass 53 | 54 | 55 | class BaseProvider(BaseConfig): 56 | """ 57 | Base class for Objets Mapping providers that implement the BaseConfig interface. 58 | 59 | """ 60 | 61 | def __init__(self, host: str, db_name: str, user: str, password: str, port: int): 62 | """ 63 | Initializes the BaseOdmProvider instance. 64 | 65 | Args: 66 | host (str): The database host. 67 | db_name (str): The name of the database. 68 | port (int): The database port number. 69 | user (str): The username for database authentication. 70 | password (str): The password for database authentication. 71 | 72 | """ 73 | self.host = host 74 | self.db_name = db_name 75 | self.user = user 76 | self.password = password 77 | self.port = port 78 | 79 | def get_engine_url(self) -> str: 80 | """ 81 | Returns the engine URL for the ORM. 82 | 83 | Returns: 84 | str: The engine URL. 85 | 86 | """ 87 | pass 88 | -------------------------------------------------------------------------------- /nest/core/database/odm_config.py: -------------------------------------------------------------------------------- 1 | from nest.core.database.base_config import BaseProvider, ConfigFactoryBase 2 | from urllib.parse import urlencode 3 | 4 | 5 | class MongoDBConfig(BaseProvider): 6 | """ 7 | ODM configuration for MongoDB. 8 | 9 | Args: 10 | host (str): The database host. 11 | db_name (str): The name of the database. 12 | user (str): The username for database authentication. 13 | password (str): The password for database authentication. 14 | port (int): The database port number. 15 | srv (bool): Whether to use the SRV connection string. 16 | uri (str): Optional pre-built MongoDB URI. 17 | **kwargs: Additional keyword arguments to include in the URI query parameters. 18 | 19 | """ 20 | 21 | def __init__( 22 | self, 23 | host: str = None, 24 | db_name: str = None, 25 | user: str = None, 26 | password: str = None, 27 | port: int = 27017, 28 | srv: bool = False, 29 | uri: str = None, 30 | **kwargs, 31 | ): 32 | """ 33 | Initializes the MongoDBConfig instance. 34 | 35 | Args: 36 | host (str): The database host. 37 | db_name (str): The name of the database. 38 | user (str): The username for database authentication. 39 | password (str): The password for database authentication. 40 | port (int): The database port number. 41 | srv (bool): Whether to use the SRV connection string. 42 | uri (str): Optional pre-built MongoDB URI. 43 | **kwargs: Additional keyword arguments to include in the URI query parameters. 44 | 45 | """ 46 | self.srv = srv 47 | self.uri = uri 48 | self.kwargs = kwargs or {} 49 | super().__init__(host, db_name, user, password, port) 50 | 51 | def get_engine_url(self) -> str: 52 | """ 53 | Returns the engine URL for the ODM. 54 | 55 | Returns: 56 | str: The engine URL. 57 | 58 | """ 59 | if self.uri: 60 | return self.uri 61 | 62 | # Build the base URI 63 | protocol = "mongodb+srv" if self.srv else "mongodb" 64 | credentials = "" 65 | if self.user and self.password: 66 | credentials = f"{self.user}:{self.password}@" 67 | elif self.user: 68 | credentials = f"{self.user}@" 69 | 70 | host_port = self.host or "" 71 | if not self.srv and self.port: 72 | host_port = f"{host_port}:{self.port}" 73 | 74 | db_name = f"/{self.db_name}" if self.db_name else "" 75 | 76 | # Build the query string from kwargs 77 | query = "" 78 | if self.kwargs: 79 | query = "?" + urlencode(self.kwargs) 80 | 81 | # Build the full URI 82 | uri = f"{protocol}://{credentials}{host_port}{db_name}{query}" 83 | return uri 84 | 85 | 86 | class ConfigFactory(ConfigFactoryBase): 87 | """ 88 | Factory class for retrieving the appropriate ORM configuration based on the database type. 89 | 90 | Args: 91 | db_type (str): The type of database. 92 | 93 | """ 94 | 95 | def __init__(self, db_type: str): 96 | """ 97 | Initializes the ConfigFactory instance. 98 | 99 | Args: 100 | db_type (str): The type of database. 101 | 102 | """ 103 | super().__init__(db_type) 104 | 105 | def get_config(self): 106 | """ 107 | Returns the appropriate ODM configuration class based on the database type. 108 | 109 | Returns: 110 | class: The ODM configuration class. 111 | 112 | Raises: 113 | Exception: If the database type is not supported. 114 | 115 | """ 116 | if self.db_type == "mongodb": 117 | return MongoDBConfig 118 | else: 119 | raise Exception(f"Database type {self.db_type} is not supported") 120 | -------------------------------------------------------------------------------- /nest/core/database/odm_provider.py: -------------------------------------------------------------------------------- 1 | from typing import List, Type 2 | 3 | from beanie import Document, init_beanie 4 | from motor.motor_asyncio import AsyncIOMotorClient 5 | 6 | from nest.core.database.odm_config import ConfigFactory 7 | 8 | 9 | class OdmProvider: 10 | """ 11 | Provides an interface for working with an ODM (Object-Document Mapping). 12 | 13 | Args: 14 | db_type (str, optional): The type of database. Defaults to "mongodb". 15 | config_params (dict, optional): Configuration parameters specific to the chosen database type. 16 | Defaults to None. 17 | document_models (List[Type[Document]]): A list of beanie.Document subclasses. 18 | 19 | Attributes: 20 | config: The configuration factory for the chosen database type. 21 | config_url: The URL generated from the database configuration parameters. 22 | 23 | """ 24 | 25 | def __init__( 26 | self, 27 | db_type="mongodb", 28 | config_params: dict = None, 29 | document_models: list[Type[Document]] = None, 30 | ): 31 | """ 32 | Initializes the OdmProvider instance. 33 | 34 | Args: 35 | db_type (str, optional): The type of database. Defaults to "mongodb". 36 | config_params (dict, optional): Configuration parameters specific to the chosen database type. 37 | Defaults to None. 38 | document_models (List[Type[Document]]): A list of beanie.Document subclasses. 39 | """ 40 | self.config_object = ConfigFactory(db_type=db_type).get_config() 41 | self.config = self.config_object(**config_params) 42 | self.config_url = self.config.get_engine_url() 43 | self.document_models = document_models or [] 44 | 45 | async def create_all(self): 46 | """ 47 | Initializes the Beanie ODM with the provided document models. 48 | """ 49 | self.check_document_models() 50 | client = AsyncIOMotorClient(self.config_url) 51 | await init_beanie( 52 | database=client.get_default_database(), document_models=self.document_models 53 | ) 54 | 55 | def check_document_models(self): 56 | """ 57 | Checks that the document_models argument is a list of beanie.Document subclasses. 58 | """ 59 | if not isinstance(self.document_models, list): 60 | raise Exception("document_models should be a list") 61 | for document_model in self.document_models: 62 | if not issubclass(document_model, Document): 63 | raise Exception( 64 | "Each item in document_models should be a subclass of beanie.Document" 65 | ) 66 | -------------------------------------------------------------------------------- /nest/core/database/orm_provider.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from contextlib import asynccontextmanager 3 | from typing import Any, Dict 4 | 5 | from sqlalchemy import create_engine 6 | from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine 7 | from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker 8 | 9 | from nest.core.database.orm_config import AsyncConfigFactory, ConfigFactory 10 | 11 | 12 | class Base(DeclarativeBase): 13 | """ 14 | Base class for ORM models. 15 | """ 16 | 17 | pass 18 | 19 | 20 | class BaseOrmProvider(ABC): 21 | def __init__( 22 | self, 23 | db_type: str = "postgresql", 24 | config_params: dict = None, 25 | async_mode: bool = False, 26 | **kwargs, 27 | ): 28 | """ 29 | Initializes the BaseOrmProvider instance. 30 | 31 | Args: 32 | db_type (str): The type of database. Defaults to "postgresql". 33 | config_params (dict): Configuration parameters for the database. 34 | async_mode (bool): Flag to indicate if the provider is asynchronous. 35 | """ 36 | self.Base = Base 37 | 38 | config_factory = AsyncConfigFactory if async_mode else ConfigFactory 39 | 40 | engine_function = create_async_engine if async_mode else create_engine 41 | if "engine_params" in kwargs: 42 | engine_params: Dict[str, Any] = kwargs.pop("engine_params") 43 | else: 44 | engine_params = {} 45 | 46 | session_function = async_sessionmaker if async_mode else sessionmaker 47 | if "session_params" in kwargs: 48 | session_params: Dict[str, Any] = kwargs.pop("session_params") 49 | else: 50 | session_params = {} 51 | 52 | self.config = config_factory(db_type=db_type).get_config() 53 | self.config_url = self.config(**config_params).get_engine_url() 54 | self.engine = engine_function(self.config_url, **engine_params) 55 | self.session = session_function(self.engine, **session_params) 56 | 57 | @abstractmethod 58 | def create_all(self): 59 | pass 60 | 61 | @abstractmethod 62 | def drop_all(self): 63 | pass 64 | 65 | @abstractmethod 66 | def get_db(self): 67 | pass 68 | 69 | 70 | class OrmProvider(BaseOrmProvider): 71 | """ 72 | Synchronous ORM provider. 73 | """ 74 | 75 | def __init__(self, db_type: str = "postgresql", config_params: dict = None): 76 | super().__init__(db_type=db_type, config_params=config_params) 77 | 78 | def create_all(self): 79 | self.Base.metadata.create_all(bind=self.engine) 80 | 81 | def drop_all(self): 82 | self.Base.metadata.drop_all(bind=self.engine) 83 | 84 | def get_db(self) -> Session: 85 | db = self.session() 86 | try: 87 | return db 88 | except Exception as e: 89 | raise e 90 | finally: 91 | db.close() 92 | 93 | 94 | class AsyncOrmProvider(BaseOrmProvider): 95 | """ 96 | Asynchronous ORM provider. 97 | """ 98 | 99 | def __init__( 100 | self, db_type: str = "postgresql", config_params: dict = None, **kwargs 101 | ): 102 | echo = kwargs.get("echo", True) 103 | kwargs["engine_params"] = dict(echo=echo) 104 | kwargs["session_params"] = dict(expire_on_commit=False, class_=AsyncSession) 105 | super().__init__( 106 | db_type=db_type, config_params=config_params, async_mode=True, **kwargs 107 | ) 108 | 109 | async def create_all(self): 110 | async with self.engine.begin() as conn: 111 | await conn.run_sync(self.Base.metadata.create_all) 112 | 113 | async def drop_all(self): 114 | async with self.engine.begin() as conn: 115 | await conn.run_sync(self.Base.metadata.drop_all) 116 | 117 | async def get_db(self) -> AsyncSession: 118 | db = self.session() 119 | try: 120 | yield db 121 | finally: 122 | await db.close() 123 | 124 | @asynccontextmanager 125 | async def get_session(self) -> AsyncSession: 126 | db = self.session() 127 | try: 128 | yield db 129 | finally: 130 | await db.close() 131 | -------------------------------------------------------------------------------- /nest/core/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | from nest.core.decorators.controller import Controller 2 | from nest.core.decorators.http_code import HttpCode 3 | from nest.core.decorators.http_method import Delete, Get, Patch, Post, Put 4 | from nest.core.decorators.injectable import Injectable 5 | from nest.core.decorators.module import Module 6 | -------------------------------------------------------------------------------- /nest/core/decorators/class_based_view.py: -------------------------------------------------------------------------------- 1 | """ 2 | Credit: FastAPI-Utils 3 | Source: https://github.com/dmontagu/fastapi-utils/blob/master/fastapi_utils/cbv.py 4 | """ 5 | 6 | import inspect 7 | from typing import ( 8 | Any, 9 | Callable, 10 | ClassVar, 11 | List, 12 | Type, 13 | TypeVar, 14 | Union, 15 | get_origin, 16 | get_type_hints, 17 | ) 18 | 19 | from fastapi import APIRouter, Depends 20 | from starlette.routing import Route, WebSocketRoute 21 | 22 | T = TypeVar("T") 23 | K = TypeVar("K", bound=Callable[..., Any]) 24 | 25 | CBV_CLASS_KEY = "__cbv_class__" 26 | 27 | 28 | def class_based_view(router: APIRouter, cls: Type[T]) -> Type[T]: 29 | """ 30 | Replaces any methods of the provided class `cls` that are endpoints of routes in `router` with updated 31 | function calls that will properly inject an instance of `cls`. 32 | """ 33 | _init_cbv(cls) 34 | cbv_router = APIRouter() 35 | function_members = inspect.getmembers(cls, inspect.isfunction) 36 | functions_set = set(func for _, func in function_members) 37 | cbv_routes = [ 38 | route 39 | for route in router.routes 40 | if isinstance(route, (Route, WebSocketRoute)) 41 | and route.endpoint in functions_set 42 | ] 43 | for route in cbv_routes: 44 | router.routes.remove(route) 45 | _update_cbv_route_endpoint_signature(cls, route) 46 | cbv_router.routes.append(route) 47 | router.include_router(cbv_router) 48 | return cls 49 | 50 | 51 | def _init_cbv(cls: Type[Any]) -> None: 52 | """ 53 | Idempotently modifies the provided `cls`, performing the following modifications: 54 | * The `__init__` function is updated to set any class-annotated dependencies as instance attributes 55 | * The `__signature__` attribute is updated to indicate to FastAPI what arguments should be passed to the initializer 56 | """ 57 | if getattr(cls, CBV_CLASS_KEY, False): # pragma: no cover 58 | return # Already initialized 59 | old_init: Callable[..., Any] = cls.__init__ 60 | old_signature = inspect.signature(old_init) 61 | old_parameters = list(old_signature.parameters.values())[ 62 | 1: 63 | ] # drop `self` parameter 64 | new_parameters = [ 65 | x 66 | for x in old_parameters 67 | if x.kind 68 | not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) 69 | ] 70 | dependency_names: List[str] = [] 71 | for name, hint in get_type_hints(cls).items(): 72 | if get_origin(hint) is ClassVar: 73 | continue 74 | parameter_kwargs = {"default": getattr(cls, name, Ellipsis)} 75 | dependency_names.append(name) 76 | new_parameters.append( 77 | inspect.Parameter( 78 | name=name, 79 | kind=inspect.Parameter.KEYWORD_ONLY, 80 | annotation=hint, 81 | **parameter_kwargs, 82 | ) 83 | ) 84 | new_signature = old_signature.replace(parameters=new_parameters) 85 | 86 | def new_init(self: Any, *args: Any, **kwargs: Any) -> None: 87 | for dep_name in dependency_names: 88 | dep_value = kwargs.pop(dep_name) 89 | setattr(self, dep_name, dep_value) 90 | old_init(self, *args, **kwargs) 91 | 92 | setattr(cls, "__signature__", new_signature) 93 | setattr(cls, "__init__", new_init) 94 | setattr(cls, CBV_CLASS_KEY, True) 95 | 96 | 97 | def _update_cbv_route_endpoint_signature( 98 | cls: Type[Any], route: Union[Route, WebSocketRoute] 99 | ) -> None: 100 | """ 101 | Fixes the endpoint signature for a cbv route to ensure FastAPI performs dependency injection properly. 102 | """ 103 | old_endpoint = route.endpoint 104 | old_signature = inspect.signature(old_endpoint) 105 | old_parameters: List[inspect.Parameter] = list(old_signature.parameters.values()) 106 | old_first_parameter = old_parameters[0] 107 | new_first_parameter = old_first_parameter.replace(default=Depends(cls)) 108 | new_parameters = [new_first_parameter] + [ 109 | parameter.replace(kind=inspect.Parameter.KEYWORD_ONLY) 110 | for parameter in old_parameters[1:] 111 | ] 112 | new_signature = old_signature.replace(parameters=new_parameters) 113 | setattr(route.endpoint, "__signature__", new_signature) -------------------------------------------------------------------------------- /nest/core/decorators/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/nest/core/decorators/cli/__init__.py -------------------------------------------------------------------------------- /nest/core/decorators/cli/cli_decorators.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | import click 4 | 5 | from nest.core import Controller 6 | from nest.core.decorators.utils import ( 7 | get_instance_variables, 8 | parse_dependencies, 9 | parse_params, 10 | ) 11 | 12 | 13 | def CliController(name: str, **kwargs): 14 | def decorator(cls): 15 | dependencies = parse_dependencies(cls) 16 | setattr(cls, "__dependencies__", dependencies) 17 | non_dep = get_instance_variables(cls) 18 | for key, value in non_dep.items(): 19 | setattr(cls, key, value) 20 | try: 21 | delattr(cls, "__init__") 22 | except AttributeError: 23 | raise AttributeError("Class must have an __init__ method") 24 | 25 | cli_group = click.Group(name, **kwargs) 26 | setattr(cls, "_cli_group", cli_group) 27 | 28 | for attr_name, method in cls.__dict__.items(): 29 | if callable(method) and hasattr(method, "_cli_command"): 30 | params = parse_params(method) 31 | cli_command = click.Command( 32 | name=method._cli_command.name, 33 | callback=partial(method, cls), 34 | params=params, 35 | ) 36 | cli_group.add_command(cli_command) 37 | 38 | return cls 39 | 40 | return decorator 41 | 42 | 43 | def CliCommand(name: str, **kwargs): 44 | def decorator(func): 45 | params = parse_params(func) 46 | func._cli_command = click.Command( 47 | name, callback=func, params=params, help=kwargs.get("help", None) 48 | ) 49 | return func 50 | 51 | return decorator 52 | -------------------------------------------------------------------------------- /nest/core/decorators/controller.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Type, List 2 | 3 | from fastapi.routing import APIRouter 4 | from fastapi import Depends 5 | 6 | from nest.core.decorators.class_based_view import class_based_view as ClassBasedView 7 | from nest.core.decorators.http_method import HTTPMethod 8 | from nest.core.decorators.utils import get_instance_variables, parse_dependencies 9 | from nest.core.decorators.guards import BaseGuard 10 | 11 | 12 | def Controller(prefix: Optional[str] = None, tag: Optional[str] = None): 13 | """ 14 | Decorator that turns a class into a controller, allowing you to define 15 | routes using FastAPI decorators. 16 | 17 | Args: 18 | prefix (str, optional): The prefix to use for all routes. 19 | tag (str, optional): The tag to use for OpenAPI documentation. 20 | 21 | Returns: 22 | class: The decorated class. 23 | """ 24 | 25 | # Default route_prefix to tag_name if route_prefix is not provided 26 | route_prefix = process_prefix(prefix, tag) 27 | 28 | def wrapper(cls: Type) -> Type[ClassBasedView]: 29 | router = APIRouter(tags=[tag] if tag else None) 30 | 31 | # Process class dependencies 32 | process_dependencies(cls) 33 | 34 | # Set instance variables 35 | set_instance_variables(cls) 36 | 37 | # Ensure the class has an __init__ method 38 | ensure_init_method(cls) 39 | 40 | # Add routes to the router 41 | add_routes(cls, router, route_prefix) 42 | 43 | # Add get_router method to the class 44 | cls.get_router = classmethod(lambda cls: router) 45 | 46 | return ClassBasedView(router=router, cls=cls) 47 | 48 | return wrapper 49 | 50 | 51 | def process_prefix(route_prefix: Optional[str], tag_name: Optional[str]) -> str: 52 | """Process and format the prefix.""" 53 | if route_prefix is None: 54 | if tag_name is None: 55 | return None 56 | else: 57 | route_prefix = tag_name 58 | 59 | if not route_prefix.startswith("/"): 60 | route_prefix = "/" + route_prefix 61 | 62 | if route_prefix.endswith("/"): 63 | route_prefix = route_prefix.rstrip("/") 64 | 65 | return route_prefix 66 | 67 | 68 | def process_dependencies(cls: Type) -> None: 69 | """Parse and set dependencies for the class.""" 70 | dependencies = parse_dependencies(cls) 71 | setattr(cls, "__dependencies__", dependencies) 72 | 73 | 74 | def set_instance_variables(cls: Type) -> None: 75 | """Set instance variables for the class.""" 76 | non_dependency_vars = get_instance_variables(cls) 77 | for key, value in non_dependency_vars.items(): 78 | setattr(cls, key, value) 79 | 80 | 81 | def ensure_init_method(cls: Type) -> None: 82 | """Ensure the class has an __init__ method.""" 83 | if not hasattr(cls, "__init__"): 84 | raise AttributeError("Class must have an __init__ method") 85 | try: 86 | delattr(cls, "__init__") 87 | except AttributeError: 88 | pass 89 | 90 | 91 | def add_routes(cls: Type, router: APIRouter, route_prefix: str) -> None: 92 | """Add routes from class methods to the router.""" 93 | for method_name, method_function in cls.__dict__.items(): 94 | if callable(method_function) and hasattr(method_function, "__http_method__"): 95 | validate_method_decorator(method_function, method_name) 96 | configure_method_route(method_function, route_prefix) 97 | add_route_to_router(router, method_function, cls) 98 | 99 | 100 | def validate_method_decorator(method_function: callable, method_name: str) -> None: 101 | """Validate that the method has a proper HTTP method decorator.""" 102 | if ( 103 | not hasattr(method_function, "__route_path__") 104 | or not method_function.__route_path__ 105 | ): 106 | raise AssertionError(f"Missing path for method {method_name}") 107 | 108 | if not isinstance(method_function.__http_method__, HTTPMethod): 109 | raise AssertionError(f"Invalid method {method_function.__http_method__}") 110 | 111 | 112 | def configure_method_route(method_function: callable, route_prefix: str) -> None: 113 | """Configure the route for the method.""" 114 | if not method_function.__route_path__.startswith("/"): 115 | method_function.__route_path__ = "/" + method_function.__route_path__ 116 | 117 | method_function.__route_path__ = ( 118 | route_prefix + method_function.__route_path__ 119 | if route_prefix 120 | else method_function.__route_path__ 121 | ) 122 | 123 | # remove trailing "/" fro __route_path__ 124 | # it converts "/api/users/" to "/api/users" 125 | if ( 126 | method_function.__route_path__ != "/" 127 | and method_function.__route_path__.endswith("/") 128 | ): 129 | method_function.__route_path__ = method_function.__route_path__.rstrip("/") 130 | 131 | 132 | def _collect_guards(cls: Type, method: callable) -> List[BaseGuard]: 133 | guards: List[BaseGuard] = [] 134 | for guard in getattr(cls, "__guards__", []): 135 | guards.append(guard) 136 | for guard in getattr(method, "__guards__", []): 137 | guards.append(guard) 138 | return guards 139 | 140 | 141 | def add_route_to_router( 142 | router: APIRouter, method_function: callable, cls: Type 143 | ) -> None: 144 | """Add the configured route to the router.""" 145 | route_kwargs = { 146 | "path": method_function.__route_path__, 147 | "endpoint": method_function, 148 | "methods": [method_function.__http_method__.value], 149 | **method_function.__kwargs__, 150 | } 151 | 152 | if hasattr(method_function, "status_code"): 153 | route_kwargs["status_code"] = method_function.status_code 154 | 155 | guards = _collect_guards(cls, method_function) 156 | if guards: 157 | dependencies = route_kwargs.get("dependencies", []) 158 | for guard in guards: 159 | dependencies.append(guard.as_dependency()) 160 | route_kwargs["dependencies"] = dependencies 161 | 162 | router.add_api_route(**route_kwargs) 163 | -------------------------------------------------------------------------------- /nest/core/decorators/database.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | from fastapi.exceptions import HTTPException 5 | from sqlalchemy.ext.asyncio import AsyncSession 6 | 7 | logging.basicConfig(level=logging.DEBUG) 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def db_request_handler(func): 12 | """ 13 | Decorator that handles database requests, including error handling and session management. 14 | 15 | Args: 16 | func (function): The function to be decorated. 17 | 18 | Returns: 19 | function: The decorated function. 20 | """ 21 | 22 | def wrapper(self, *args, **kwargs): 23 | try: 24 | s = time.time() 25 | result = func(self, *args, **kwargs) 26 | p_time = time.time() - s 27 | logging.info(f"request finished after {p_time}") 28 | if hasattr(self, "session"): 29 | # Check if self is an instance of OrmService 30 | self.session.close() 31 | return result 32 | except Exception as e: 33 | logging.error(e) 34 | if hasattr(self, "session"): 35 | # Check if self is an instance of OrmService 36 | self.session.rollback() 37 | self.session.close() 38 | return HTTPException(status_code=500, detail=str(e)) 39 | 40 | return wrapper 41 | 42 | 43 | def async_db_request_handler(func): 44 | """ 45 | Asynchronous decorator that handles database requests, including error handling, 46 | session management, and logging for async functions. 47 | 48 | Args: 49 | func (function): The async function to be decorated. 50 | 51 | Returns: 52 | function: The decorated async function. 53 | """ 54 | 55 | async def wrapper(*args, **kwargs): 56 | try: 57 | start_time = time.time() 58 | result = await func(*args, **kwargs) # Awaiting the async function 59 | process_time = time.time() - start_time 60 | logger.info(f"Async request finished after {process_time} seconds") 61 | return result 62 | except Exception as e: 63 | self = args[0] if args else None 64 | session = getattr(self, "session", None) 65 | # If not found, check in function arguments 66 | if session: 67 | session_type = "class" 68 | else: 69 | session = [arg for arg in args if isinstance(arg, AsyncSession)][0] 70 | if session: 71 | session_type = "function" 72 | else: 73 | raise ValueError("AsyncSession not provided to the function") 74 | 75 | logger.error(f"Error in async request: {e}") 76 | # Rollback if session is in a transaction 77 | if session and session_type == "function" and session.in_transaction(): 78 | await session.rollback() 79 | elif session and session_type == "class": 80 | async with session() as session: 81 | await session.rollback() 82 | raise Exception(f"Error in async request: {e}") 83 | 84 | return wrapper 85 | -------------------------------------------------------------------------------- /nest/core/decorators/http_code.py: -------------------------------------------------------------------------------- 1 | from nest.common.constants import STATUS_CODE_TOKEN 2 | 3 | 4 | def HttpCode(status_code: int): 5 | """ 6 | Decorator that sets the HTTP status code for a route. 7 | 8 | Args: 9 | status_code (int): The HTTP status code for the response. 10 | """ 11 | 12 | def decorator(func): 13 | if not hasattr(func, STATUS_CODE_TOKEN): 14 | setattr(func, STATUS_CODE_TOKEN, status_code) 15 | return func 16 | 17 | return decorator 18 | -------------------------------------------------------------------------------- /nest/core/decorators/http_method.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, Callable, List, Union 3 | 4 | 5 | class HTTPMethod(Enum): 6 | GET = "GET" 7 | POST = "POST" 8 | DELETE = "DELETE" 9 | PUT = "PUT" 10 | PATCH = "PATCH" 11 | HEAD = "HEAD" 12 | OPTIONS = "OPTIONS" 13 | 14 | 15 | def route(http_method: HTTPMethod, route_path: Union[str, List[str]] = "/", **kwargs): 16 | """ 17 | Decorator that defines a route for the controller. 18 | 19 | Args: 20 | http_method (HTTPMethod): The HTTP method for the route (GET, POST, DELETE, PUT, PATCH). 21 | route_path (Union[str, List[str]]): The route path for the route. example: "/users" 22 | **kwargs: Additional keyword arguments to configure the route. 23 | 24 | Returns: 25 | function: The decorated function. 26 | """ 27 | 28 | def decorator(func): 29 | func.__http_method__ = http_method 30 | func.__route_path__ = route_path 31 | func.__kwargs__ = kwargs 32 | 33 | return func 34 | 35 | return decorator 36 | 37 | 38 | def Get(route_path: Union[str, List[str]] = "/", **kwargs) -> Callable[..., Any]: 39 | return route(HTTPMethod.GET, route_path, **kwargs) 40 | 41 | 42 | def Post(route_path: Union[str, List[str]] = "/", **kwargs) -> Callable[..., Any]: 43 | return route(HTTPMethod.POST, route_path, **kwargs) 44 | 45 | 46 | def Delete(route_path: Union[str, List[str]] = "/", **kwargs) -> Callable[..., Any]: 47 | return route(HTTPMethod.DELETE, route_path, **kwargs) 48 | 49 | 50 | def Put(route_path: Union[str, List[str]] = "/", **kwargs) -> Callable[..., Any]: 51 | return route(HTTPMethod.PUT, route_path, **kwargs) 52 | 53 | 54 | def Patch(route_path: Union[str, List[str]] = "/", **kwargs) -> Callable[..., Any]: 55 | return route(HTTPMethod.PATCH, route_path, **kwargs) 56 | 57 | 58 | def Head(route_path: Union[str, List[str]] = "/", **kwargs) -> Callable[..., Any]: 59 | return route(HTTPMethod.HEAD, route_path, **kwargs) 60 | 61 | 62 | def Options(route_path: Union[str, List[str]] = "/", **kwargs) -> Callable[..., Any]: 63 | return route(HTTPMethod.OPTIONS, route_path, **kwargs) 64 | -------------------------------------------------------------------------------- /nest/core/decorators/injectable.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Optional, Type 2 | 3 | from injector import inject 4 | 5 | from nest.common.constants import DEPENDENCIES, INJECTABLE_NAME, INJECTABLE_TOKEN 6 | from nest.core.decorators.utils import parse_dependencies 7 | 8 | 9 | def Injectable(target_class: Optional[Type] = None, *args, **kwargs) -> Callable: 10 | """ 11 | Decorator to mark a class as injectable and handle its dependencies. 12 | 13 | Args: 14 | target_class (Type, optional): The class to be decorated. 15 | 16 | Returns: 17 | Callable: The decorator function. 18 | """ 19 | 20 | def decorator(decorated_class: Type) -> Type: 21 | """ 22 | Inner decorator function to process the class. 23 | 24 | Args: 25 | decorated_class (Type): The class to be processed. 26 | 27 | Returns: 28 | Type: The processed class with dependencies injected. 29 | """ 30 | 31 | if "__init__" not in decorated_class.__dict__: 32 | 33 | def init_method(self, *args, **kwargs): 34 | pass 35 | 36 | decorated_class.__init__ = init_method 37 | 38 | dependencies = parse_dependencies(decorated_class) 39 | 40 | setattr(decorated_class, DEPENDENCIES, dependencies) 41 | setattr(decorated_class, INJECTABLE_TOKEN, True) 42 | setattr(decorated_class, INJECTABLE_NAME, decorated_class.__name__) 43 | 44 | inject(decorated_class) 45 | 46 | return decorated_class 47 | 48 | if target_class is not None: 49 | return decorator(target_class) 50 | 51 | return decorator 52 | -------------------------------------------------------------------------------- /nest/core/decorators/module.py: -------------------------------------------------------------------------------- 1 | from nest.common.constants import ModuleMetadata 2 | from typing import List 3 | import dataclasses 4 | 5 | 6 | @dataclasses.dataclass(frozen=True) 7 | class Module: 8 | controllers: List[type] = dataclasses.field(default_factory=list) 9 | providers: List[type] = dataclasses.field(default_factory=list) 10 | exports: List[type] = dataclasses.field(default_factory=list) 11 | imports: List[type] = dataclasses.field(default_factory=list) 12 | is_global: bool = dataclasses.field(default=False) 13 | 14 | def __call__(self, cls): 15 | setattr(cls, ModuleMetadata.CONTROLLERS, self.controllers) 16 | setattr(cls, ModuleMetadata.PROVIDERS, self.providers) 17 | setattr(cls, ModuleMetadata.IMPORTS, self.imports) 18 | setattr(cls, ModuleMetadata.EXPORTS, self.exports) 19 | setattr(cls, "__is_module__", True) 20 | setattr(cls, "__is_global__", self.is_global) 21 | 22 | return cls 23 | -------------------------------------------------------------------------------- /nest/core/decorators/utils.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import inspect 3 | from typing import Callable, List 4 | 5 | import click 6 | 7 | from nest.common.constants import INJECTABLE_TOKEN 8 | 9 | 10 | def get_instance_variables(cls): 11 | """ 12 | Retrieves instance variables assigned in the __init__ method of a class, 13 | excluding those that are injected dependencies. 14 | 15 | Args: 16 | cls (type): The class to inspect. 17 | 18 | Returns: 19 | dict: A dictionary with variable names as keys and their assigned values. 20 | """ 21 | try: 22 | source = inspect.getsource(cls.__init__).strip() 23 | tree = ast.parse(source) 24 | 25 | # Getting the parameter names to exclude dependencies 26 | dependencies = set( 27 | param.name 28 | for param in inspect.signature(cls.__init__).parameters.values() 29 | if param.annotation != param.empty 30 | and getattr(param.annotation, "__injectable__", False) 31 | ) 32 | 33 | instance_vars = {} 34 | for node in ast.walk(tree): 35 | if isinstance(node, ast.Assign): 36 | for target in node.targets: 37 | if ( 38 | isinstance(target, ast.Attribute) 39 | and isinstance(target.value, ast.Name) 40 | and target.value.id == "self" 41 | ): 42 | # Exclude dependencies 43 | if target.attr not in dependencies: 44 | # Here you can either store the source code of the value or 45 | # evaluate it in the class' context, depending on your needs 46 | instance_vars[target.attr] = ast.get_source_segment( 47 | source, node.value 48 | ) 49 | return instance_vars 50 | except Exception as e: 51 | return {} 52 | 53 | 54 | def get_non_dependencies_params(cls): 55 | source = inspect.getsource(cls.__init__).strip() 56 | tree = ast.parse(source) 57 | non_dependencies = {} 58 | for node in ast.walk(tree): 59 | if isinstance(node, ast.Attribute): 60 | non_dependencies[node.attr] = node.value.id 61 | return non_dependencies 62 | 63 | 64 | def parse_dependencies(cls): 65 | signature = inspect.signature(cls.__init__) 66 | dependecies = {} 67 | for param in signature.parameters.values(): 68 | try: 69 | if ( 70 | param.annotation != param.empty 71 | and hasattr(param.annotation, "__dict__") 72 | and INJECTABLE_TOKEN in param.annotation.__dict__ 73 | ): 74 | dependecies[param.name] = param.annotation 75 | except Exception as e: 76 | raise e 77 | return dependecies 78 | 79 | 80 | def parse_params(func: Callable) -> List[click.Option]: 81 | signature = inspect.signature(func) 82 | params = [] 83 | for param in signature.parameters.values(): 84 | try: 85 | if param.annotation != param.empty: 86 | params.append(param.annotation) 87 | except Exception as e: 88 | raise e 89 | return params 90 | -------------------------------------------------------------------------------- /nest/core/pynest_app_context.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import TypeVar, Union 3 | 4 | from nest.common.exceptions import UnknownModuleException 5 | from nest.common.module import Module, ModuleCompiler 6 | from nest.core.pynest_container import PyNestContainer 7 | 8 | T = TypeVar("T") 9 | 10 | 11 | class PyNestApplicationContext: 12 | """ 13 | Represents the application context in a PyNest application. 14 | This class is responsible for managing the application's lifecycle and modules. 15 | 16 | Attributes: 17 | container (PyNestContainer): The container that holds the application's modules and manages the dependency injection. 18 | context_module (Module, optional): The module that represents the current context of the application. 19 | logger (Logger): Logger for the application context. 20 | 21 | Args: 22 | container (PyNestContainer): The container for the application. 23 | context_module (Union[Module, None], optional): The initial context module of the application. 24 | """ 25 | 26 | _is_initialized = False 27 | _module_compiler = ModuleCompiler() 28 | 29 | @property 30 | def is_initialized(self): 31 | """ 32 | Property to check if the application context is initialized. 33 | 34 | Returns: 35 | bool: True if the application context is initialized, False otherwise. 36 | """ 37 | return self._is_initialized 38 | 39 | @is_initialized.setter 40 | def is_initialized(self, value: bool): 41 | """ 42 | Setter for the is_initialized property. 43 | 44 | Args: 45 | value (bool): The new value for the is_initialized property. 46 | """ 47 | self._is_initialized = value 48 | 49 | def init(self): 50 | """ 51 | Initializes the application context. If the context is already initialized, it returns itself. 52 | 53 | Returns: 54 | PyNestApplicationContext: The initialized application context. 55 | """ 56 | if self._is_initialized: 57 | return self 58 | 59 | self._is_initialized = True 60 | return self 61 | 62 | def __init__( 63 | self, container: PyNestContainer, context_module: Union[Module, None] = None 64 | ): 65 | """ 66 | Constructor for the PyNestApplicationContext. 67 | 68 | Args: 69 | container (PyNestContainer): The container for the application. 70 | context_module (Union[Module, None], optional): The initial context module of the application. 71 | """ 72 | self.container = container 73 | self.context_module: Module = context_module 74 | self.logger = logging.getLogger(PyNestApplicationContext.__name__) 75 | 76 | def select_context_module(self): 77 | """ 78 | Selects the first module from the container as the context module. 79 | """ 80 | modules = self.container.modules.values() 81 | self.context_module = next(iter(modules), None) 82 | 83 | def select(self, module: T) -> T: 84 | """ 85 | Selects a specific module as the current context module based on the provided module class - The selected Module is the AppModule that contains all the application's graph. 86 | 87 | Args: 88 | module (Module): The module class to select. 89 | 90 | Returns: 91 | PyNestApplicationContext: A new application context with the selected module. 92 | 93 | Raises: 94 | UnknownModuleException: If the specified module is not found in the application's modules. 95 | """ 96 | modules_container = self.container.modules 97 | module_token_factory = self.container.module_token_factory 98 | 99 | metadata = self._module_compiler.extract_metadata(module) 100 | token = module_token_factory.create(metadata["type"]) 101 | selected_module = modules_container.get(token) 102 | if selected_module is None: 103 | raise UnknownModuleException() 104 | 105 | return PyNestApplicationContext(self.container, selected_module) 106 | -------------------------------------------------------------------------------- /nest/core/pynest_application.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from fastapi import FastAPI 4 | 5 | from nest.common.route_resolver import RoutesResolver 6 | from nest.core.pynest_app_context import PyNestApplicationContext 7 | from nest.core.pynest_container import PyNestContainer 8 | 9 | 10 | class PyNestApp(PyNestApplicationContext): 11 | """ 12 | PyNestApp is the main application class for the PyNest framework, 13 | managing the container and HTTP server. 14 | """ 15 | 16 | _is_listening = False 17 | 18 | @property 19 | def is_listening(self) -> bool: 20 | return self._is_listening 21 | 22 | def __init__(self, container: PyNestContainer, http_server: FastAPI): 23 | """ 24 | Initialize the PyNestApp with the given container and HTTP server. 25 | 26 | Args: 27 | container (PyNestContainer): The PyNestContainer container instance. 28 | http_server (FastAPI): The FastAPI server instance. 29 | """ 30 | self.container = container 31 | self.http_server = http_server 32 | super().__init__(self.container) 33 | self.routes_resolver = RoutesResolver(self.container, self.http_server) 34 | self.select_context_module() 35 | self.register_routes() 36 | 37 | def use(self, middleware: type, **options: Any) -> "PyNestApp": 38 | """ 39 | Add middleware to the FastAPI server. 40 | 41 | Args: 42 | middleware (type): The middleware class. 43 | **options (Any): Additional options for the middleware. 44 | 45 | Returns: 46 | PyNestApp: The current instance of PyNestApp, allowing method chaining. 47 | """ 48 | self.http_server.add_middleware(middleware, **options) 49 | return self 50 | 51 | def get_server(self) -> FastAPI: 52 | """ 53 | Get the FastAPI server instance. 54 | 55 | Returns: 56 | FastAPI: The FastAPI server instance. 57 | """ 58 | return self.http_server 59 | 60 | def register_routes(self): 61 | """ 62 | Register the routes using the RoutesResolver. 63 | """ 64 | self.routes_resolver.register_routes() 65 | -------------------------------------------------------------------------------- /nest/core/pynest_factory.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Type, TypeVar 3 | 4 | from fastapi import FastAPI 5 | 6 | from nest.core.pynest_application import PyNestApp 7 | from nest.core.pynest_container import PyNestContainer 8 | 9 | ModuleType = TypeVar("ModuleType") 10 | 11 | 12 | class AbstractPyNestFactory(ABC): 13 | @abstractmethod 14 | def create(self, main_module: Type[ModuleType], **kwargs): 15 | raise NotImplementedError 16 | 17 | 18 | class PyNestFactory(AbstractPyNestFactory): 19 | """Factory class for creating PyNest applications.""" 20 | 21 | @staticmethod 22 | def create(main_module: Type[ModuleType], **kwargs) -> PyNestApp: 23 | """ 24 | Create a PyNest application with the specified main module class. 25 | 26 | Args: 27 | main_module (ModuleType): The main module for the PyNest application. 28 | **kwargs: Additional keyword arguments for the FastAPI server. 29 | 30 | Returns: 31 | PyNestApp: The created PyNest application. 32 | """ 33 | container = PyNestContainer() 34 | container.add_module(main_module) 35 | http_server = PyNestFactory._create_server(**kwargs) 36 | return PyNestApp(container, http_server) 37 | 38 | @staticmethod 39 | def _create_server(**kwargs) -> FastAPI: 40 | """ 41 | Create a FastAPI server. 42 | 43 | Args: 44 | **kwargs: Additional keyword arguments for the FastAPI server. 45 | 46 | Returns: 47 | FastAPI: The created FastAPI server. 48 | """ 49 | return FastAPI(**kwargs) 50 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | 6 | [tool.poetry] 7 | name = "pynest-api" 8 | version = "0.4.0" 9 | description = "PyNest is a FastAPI Abstraction for building microservices, influenced by NestJS." 10 | authors = ["itay.dar "] 11 | readme = "README.md" 12 | homepage = "https://github.com/PythonNest/PyNest" 13 | documentation = "https://pythonnest.github.io/PyNest/" 14 | packages = [ 15 | { include = "nest" } 16 | ] 17 | classifiers = [ 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Operating System :: OS Independent", 23 | "License :: OSI Approved :: MIT License", 24 | "Development Status :: 4 - Beta", 25 | "Intended Audience :: Developers", 26 | "Topic :: Software Development :: Libraries :: Python Modules", 27 | ] 28 | 29 | 30 | 31 | [tool.poetry.dependencies] 32 | python = "^3.9" 33 | # Core dependencies 34 | click = "^8.1.7" 35 | injector = "^0.22.0" 36 | astor = "^0.8.1" 37 | pyyaml = "^6.0.2" 38 | fastapi = "^0.115.4" 39 | pydantic = "^2.9.2" 40 | uvicorn = "^0.32.0" 41 | 42 | 43 | # Optional dependencies 44 | sqlalchemy = { version = "^2.0.36", optional = true } 45 | asyncpg = { version = "^0.30.0", optional = true } 46 | psycopg2 = { version = "^2.9.3", optional = true } 47 | alembic = { version = "^1.13.3", optional = true } 48 | beanie = { version = "^1.27.0", optional = true } 49 | python-dotenv = { version = "^1.0.1", optional = true } 50 | greenlet = { version = "^3.1.1", optional = true } 51 | black = "^24.10.0" 52 | 53 | 54 | 55 | [tool.poetry.extras] 56 | postgres = ["sqlalchemy", "asyncpg", "psycopg2", "alembic", "greenlet", "python-dotenv"] 57 | mongo = ["beanie", "python-dotenv"] 58 | test = ["pytest"] 59 | 60 | [tool.poetry.group.build.dependencies] 61 | setuptools = "^75.3.0" 62 | wheel = "^0.44.0" 63 | build = "^1.2.2.post1" 64 | twine = "^5.1.1" 65 | git-changelog = "^2.5.2" 66 | 67 | [tool.poetry.group.test.dependencies] 68 | pytest = "^7.0.1" 69 | fastapi = "^0.115.4" 70 | sqlalchemy = "^2.0.36" 71 | motor = "^3.2.0" 72 | beanie = "^1.27.0" 73 | pydantic = "^2.9.2" 74 | python-dotenv = "^1.0.1" 75 | uvicorn = "^0.32.0" 76 | 77 | [tool.poetry.group.docs.dependencies] 78 | mkdocs-material = "^9.5.43" 79 | mkdocstrings-python = "^1.12.2" 80 | 81 | 82 | [tool.black] 83 | force-exclude = ''' 84 | /( 85 | | /*venv* 86 | | /.git 87 | | /dist 88 | | /pytest_compare.egg-info 89 | | *.bac 90 | | .mypy_cache 91 | | .coverage 92 | | /htmlcov 93 | | /docs 94 | | /site 95 | )/ 96 | ''' 97 | 98 | [tool.mypy] 99 | exclude = [ 100 | "/*venv*" 101 | ] 102 | ignore_missing_imports = true 103 | 104 | [tool.poetry.urls] 105 | Homepage = "https://github.com/PythonNest/PyNest" 106 | Documentation = "https://pythonnest.github.io/PyNest/" 107 | 108 | [tool.poetry.scripts] 109 | pynest = "nest.cli.cli:nest_cli" 110 | 111 | 112 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/tests/test_common/__init__.py -------------------------------------------------------------------------------- /tests/test_common/test_module.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/tests/test_common/test_module.py -------------------------------------------------------------------------------- /tests/test_common/test_route_resolver.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/tests/test_common/test_route_resolver.py -------------------------------------------------------------------------------- /tests/test_core/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi import FastAPI 3 | 4 | from nest.common.route_resolver import RoutesResolver 5 | from nest.core import ( 6 | Controller, 7 | Get, 8 | Injectable, 9 | Module, 10 | PyNestContainer, 11 | PyNestFactory, 12 | ) 13 | from nest.core.pynest_application import PyNestApp 14 | 15 | 16 | @Injectable 17 | class TestService: 18 | def get_message(self): 19 | return "Hello, World!" 20 | 21 | 22 | @Controller("test") 23 | class TestController: 24 | def __init__(self, test_service: TestService): 25 | self.test_service = test_service 26 | 27 | @Get("/") 28 | def get_test(self): 29 | return {"message": "GET endpoint"} 30 | 31 | @Get("/message") 32 | def get_message(self): 33 | return {"message": self.test_service.get_message()} 34 | 35 | 36 | @Module(controllers=[TestController], providers=[TestService], exports=[TestService]) 37 | class TestModule: 38 | pass 39 | 40 | 41 | @pytest.fixture 42 | def test_module(): 43 | return TestModule 44 | 45 | 46 | @pytest.fixture 47 | def test_server() -> FastAPI: 48 | server = PyNestFactory._create_server( 49 | title="Test Server", 50 | description="This is a test server", 51 | version="1.0.0", 52 | debug=True, 53 | ) 54 | return server 55 | 56 | 57 | @pytest.fixture 58 | def test_container(): 59 | return PyNestContainer() 60 | 61 | 62 | @pytest.fixture 63 | def test_resolver(test_container, test_server): 64 | return RoutesResolver(test_container, test_server) 65 | -------------------------------------------------------------------------------- /tests/test_core/test_app_context.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/tests/test_core/test_app_context.py -------------------------------------------------------------------------------- /tests/test_core/test_database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/tests/test_core/test_database/__init__.py -------------------------------------------------------------------------------- /tests/test_core/test_database/test_odm.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from nest.core.database.odm_config import ConfigFactory, MongoDBConfig 6 | from nest.core.database.odm_provider import OdmProvider 7 | 8 | 9 | @pytest.fixture(scope="module") 10 | def odm_service(): 11 | return OdmProvider( 12 | db_type="mongodb", 13 | config_params={ 14 | "db_name": "db_name", 15 | "host": "host", 16 | "user": "user", 17 | "password": "password", 18 | "port": "port", 19 | }, 20 | document_models=[], 21 | ) 22 | 23 | 24 | @pytest.fixture(scope="module") 25 | def mongodb_config(): 26 | return MongoDBConfig( 27 | db_name="db_name", host="host", user="user", password="password", port="port" 28 | ) 29 | 30 | 31 | def test_odm_service_definition(odm_service): 32 | assert odm_service.config 33 | assert odm_service.config_url 34 | assert odm_service.document_models == [] 35 | 36 | 37 | def test_odm_service_config_url(odm_service): 38 | config_url = odm_service.config_url 39 | assert config_url == "mongodb://user:password@host:port/db_name" 40 | 41 | 42 | def test_mongo_config_definition(mongodb_config): 43 | assert mongodb_config 44 | assert mongodb_config.get_engine_url 45 | -------------------------------------------------------------------------------- /tests/test_core/test_database/test_orm.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from nest.core.database.orm_config import ConfigFactory 6 | from nest.core.database.orm_provider import OrmProvider 7 | 8 | 9 | @pytest.fixture(scope="module") 10 | def config_factory(): 11 | return ConfigFactory 12 | 13 | 14 | def test_config_factory_definition(config_factory): 15 | assert config_factory 16 | assert config_factory.get_config 17 | 18 | 19 | @pytest.fixture(scope="module") 20 | def sqlite_config_factory(config_factory): 21 | config = config_factory(db_type="sqlite").get_config() 22 | params = dict(db_name=os.getenv("SQLITE_DB_NAME", "default_nest_db")) 23 | return config(**params) 24 | 25 | 26 | @pytest.fixture(scope="module") 27 | def postgres_config_factory(config_factory): 28 | config = config_factory(db_type="postgresql").get_config() 29 | params = dict( 30 | db_name=os.getenv("POSTGRES_DB_NAME", "default_nest_db"), 31 | host=os.getenv("POSTGRES_HOST", "localhost"), 32 | user=os.getenv("POSTGRES_USER", "postgres"), 33 | password=os.getenv("POSTGRES_PASSWORD", "postgres"), 34 | port=os.getenv("POSTGRES_PORT", "5432"), 35 | ) 36 | return config(**params) 37 | 38 | 39 | @pytest.fixture(scope="module") 40 | def mysql_config_factory(config_factory): 41 | config = config_factory(db_type="mysql").get_config() 42 | params = dict( 43 | db_name=os.getenv("MYSQL_DB_NAME", "default_nest_db"), 44 | host=os.getenv("MYSQL_HOST", "localhost"), 45 | user=os.getenv("MYSQL_USER", "root"), 46 | password=os.getenv("MYSQL_PASSWORD", "root"), 47 | port=os.getenv("MYSQL_PORT", "3306"), 48 | ) 49 | return config(**params) 50 | 51 | 52 | def test_sqlite_url(sqlite_config_factory): 53 | config_url = sqlite_config_factory.get_engine_url() 54 | assert config_url == "sqlite:///default_nest_db.db" 55 | 56 | 57 | def test_postgres_url(postgres_config_factory): 58 | config_url = postgres_config_factory.get_engine_url() 59 | assert ( 60 | config_url 61 | == "postgresql+psycopg2://postgres:postgres@localhost:5432/default_nest_db" 62 | ) 63 | 64 | 65 | def test_mysql_url(mysql_config_factory): 66 | config_url = mysql_config_factory.get_engine_url() 67 | assert ( 68 | config_url == "mysql+mysqlconnector://root:root@localhost:3306/default_nest_db" 69 | ) 70 | 71 | 72 | @pytest.fixture(scope="module") 73 | def sqlite_orm_service(): 74 | return OrmProvider( 75 | db_type="sqlite", 76 | config_params=dict(db_name=os.getenv("SQLITE_DB_NAME", "default_nest_db")), 77 | ) 78 | 79 | 80 | @pytest.fixture(scope="module") 81 | def postgres_orm_service(): 82 | return OrmProvider( 83 | db_type="postgresql", 84 | config_params=dict( 85 | db_name=os.getenv("POSTGRES_DB_NAME", "default_nest_db"), 86 | host=os.getenv("POSTGRES_HOST", "localhost"), 87 | user=os.getenv("POSTGRES_USER", "postgres"), 88 | password=os.getenv("POSTGRES_PASSWORD", "postgres"), 89 | port=os.getenv("POSTGRES_PORT", "5432"), 90 | ), 91 | ) 92 | 93 | 94 | @pytest.fixture(scope="module") 95 | def mysql_orm_service(): 96 | return OrmProvider( 97 | db_type="mysql", 98 | config_params=dict( 99 | db_name=os.getenv("MYSQL_DB_NAME", "default_nest_db"), 100 | host=os.getenv("MYSQL_HOST", "localhost"), 101 | user=os.getenv("MYSQL_USER", "root"), 102 | password=os.getenv("MYSQL_PASSWORD", "root"), 103 | port=os.getenv("MYSQL_PORT", "3306"), 104 | ), 105 | ) 106 | -------------------------------------------------------------------------------- /tests/test_core/test_decorators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonNest/PyNest/fb56de203153c271965a3c9ff2da42be03d6fcf0/tests/test_core/test_decorators/__init__.py -------------------------------------------------------------------------------- /tests/test_core/test_decorators/test_controller.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from nest.core import Controller, Delete, Get, Injectable, Patch, Post, Put 4 | 5 | 6 | @Controller(prefix="api/v1/user", tag="test") 7 | class TestController: 8 | def __init__(self): ... 9 | 10 | @Get("/get_all_users") 11 | def get_endpoint(self): 12 | return {"message": "GET endpoint"} 13 | 14 | @Post("/create_user") 15 | def post_endpoint(self): 16 | return {"message": "POST endpoint"} 17 | 18 | @Delete("/delete_user") 19 | def delete_endpoint(self): 20 | return {"message": "DELETE endpoint"} 21 | 22 | @Put("/update_user") 23 | def put_endpoint(self): 24 | return {"message": "PUT endpoint"} 25 | 26 | @Patch("/patch_user") 27 | def patch_endpoint(self): 28 | return {"message": "PATCH endpoint"} 29 | 30 | 31 | @pytest.fixture 32 | def test_controller(): 33 | return TestController() 34 | 35 | 36 | @pytest.mark.parametrize( 37 | "function, endpoint, expected_message", 38 | [ 39 | ("get_endpoint", "get_all_users", "GET endpoint"), 40 | ("post_endpoint", "create_user", "POST endpoint"), 41 | ("delete_endpoint", "delete_user", "DELETE endpoint"), 42 | ("put_endpoint", "update_user", "PUT endpoint"), 43 | ("patch_endpoint", "patch_user", "PATCH endpoint"), 44 | ], 45 | ) 46 | def test_endpoints(test_controller, function, endpoint, expected_message): 47 | attribute = getattr(test_controller, function) 48 | assert attribute.__route_path__ == "/api/v1/user/" + endpoint 49 | assert attribute.__kwargs__ == {} 50 | assert attribute.__http_method__.value == function.split("_")[0].upper() 51 | assert attribute() == {"message": f"{function.split('_')[0].upper()} endpoint"} 52 | -------------------------------------------------------------------------------- /tests/test_core/test_decorators/test_guard.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from fastapi import Request 4 | 5 | from fastapi.security import HTTPBearer 6 | 7 | from nest.core import Controller, Get, UseGuards, BaseGuard 8 | 9 | 10 | class SimpleGuard(BaseGuard): 11 | def __init__(self): 12 | self.called = False 13 | 14 | def can_activate(self, request: Request) -> bool: 15 | self.called = True 16 | return True 17 | 18 | 19 | class BearerGuard(BaseGuard): 20 | security_scheme = HTTPBearer() 21 | 22 | def can_activate(self, request: Request, credentials) -> bool: 23 | return True 24 | 25 | 26 | class JWTGuard(BaseGuard): 27 | security_scheme = HTTPBearer() 28 | 29 | def can_activate(self, request: Request, credentials=None) -> bool: 30 | if credentials and credentials.scheme == "Bearer": 31 | return self.validate_jwt(credentials.credentials) 32 | return False 33 | 34 | 35 | @Controller("/guard") 36 | class GuardController: 37 | @Get("/") 38 | @UseGuards(SimpleGuard) 39 | def root(self): 40 | return {"ok": True} 41 | 42 | 43 | def test_use_guards_sets_attribute(): 44 | assert hasattr(GuardController.root, "__guards__") 45 | assert SimpleGuard in GuardController.root.__guards__ 46 | 47 | 48 | def test_guard_added_to_route_dependencies(): 49 | router = GuardController.get_router() 50 | route = router.routes[0] 51 | deps = route.dependencies 52 | assert len(deps) == 1 53 | assert callable(deps[0].dependency) 54 | 55 | 56 | def _has_security_requirements(dependant): 57 | """Recursively check if a dependant or its dependencies have security requirements.""" 58 | if dependant.security_requirements: 59 | return True 60 | 61 | for dep in dependant.dependencies: 62 | if _has_security_requirements(dep): 63 | return True 64 | 65 | return False 66 | 67 | 68 | def test_openapi_security_requirement(): 69 | @Controller("/bearer") 70 | class BearerController: 71 | @Get("/") 72 | @UseGuards(BearerGuard) 73 | def root(self): 74 | return {"ok": True} 75 | 76 | router = BearerController.get_router() 77 | route = router.routes[0] 78 | 79 | # Check if security requirements exist anywhere in the dependency tree 80 | assert _has_security_requirements(route.dependant), \ 81 | "Security requirements should be present in the dependency tree for OpenAPI integration" 82 | -------------------------------------------------------------------------------- /tests/test_core/test_pynest_application.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from nest.core import PyNestApp 4 | from tests.test_core import test_container, test_resolver 5 | from tests.test_core.test_pynest_factory import test_server 6 | 7 | 8 | @pytest.fixture 9 | def pynest_app(test_container, test_server): 10 | return PyNestApp(container=test_container, http_server=test_server) 11 | 12 | 13 | def test_is_listening_property(pynest_app): 14 | assert not pynest_app.is_listening 15 | pynest_app._is_listening = ( 16 | True # Directly modify the protected attribute for testing 17 | ) 18 | assert pynest_app.is_listening 19 | 20 | 21 | def test_get_server_returns_http_server(pynest_app, test_server): 22 | assert pynest_app.get_server() == test_server 23 | 24 | 25 | def test_register_routes_calls_register_routes_on_resolver(pynest_app, test_resolver): 26 | pynest_app.routes_resolver = test_resolver 27 | pynest_app.register_routes() 28 | assert pynest_app.get_server().routes 29 | -------------------------------------------------------------------------------- /tests/test_core/test_pynest_container.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from nest.core import Module, PyNestContainer 4 | from tests.test_core import test_module 5 | 6 | 7 | @pytest.fixture(scope="module") 8 | def container(): 9 | # Since PyNestContainer is a singleton, we clear its state before each test to ensure test isolation 10 | PyNestContainer()._instance = None 11 | return PyNestContainer() 12 | 13 | 14 | def test_singleton_pattern(container): 15 | second_container = PyNestContainer() 16 | assert ( 17 | container is second_container 18 | ), "PyNestContainer should implement the singleton pattern" 19 | 20 | 21 | def test_add_module(container, test_module): 22 | result = container.add_module(test_module) 23 | assert result["inserted"] is True, "Module should be added successfully" 24 | module_ref = result["module_ref"] 25 | assert module_ref is not None, "Module reference should not be None" 26 | assert container.modules.has( 27 | module_ref.token 28 | ), "Module should be added to the container" 29 | -------------------------------------------------------------------------------- /tests/test_core/test_pynest_factory.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi import FastAPI 3 | 4 | from nest.core import PyNestFactory # Replace 'your_module' with the actual module name 5 | from tests.test_core import test_container, test_module, test_server 6 | 7 | 8 | def test_create_server(test_server): 9 | assert isinstance(test_server, FastAPI) 10 | assert test_server.title == "Test Server" 11 | assert test_server.description == "This is a test server" 12 | assert test_server.version == "1.0.0" 13 | assert test_server.debug is True 14 | 15 | 16 | def test_e2e(test_module): 17 | app = PyNestFactory.create( 18 | test_module, 19 | title="Test Server", 20 | description="This is a test server", 21 | version="1.0.0", 22 | debug=True, 23 | ) 24 | http_server = app.get_server() 25 | assert isinstance(http_server, FastAPI) 26 | assert http_server.title == "Test Server" 27 | assert http_server.description == "This is a test server" 28 | assert http_server.version == "1.0.0" 29 | assert http_server.debug is True 30 | --------------------------------------------------------------------------------