├── .build └── Dockerfile ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── docker-image.yml │ ├── lint.yml │ └── update_dep.yml ├── .gitignore ├── .pdm-python ├── README.md ├── accounts ├── __init__.py ├── endpoints.py ├── piccolo_app.py ├── piccolo_migrations │ └── __init__.py └── tables.py ├── app.py ├── conftest.py ├── docker-compose.yml ├── home ├── __init__.py ├── endpoints.py ├── piccolo_app.py ├── piccolo_migrations │ └── README.md ├── tables.py └── templates │ ├── base.html.jinja │ └── home.html.jinja ├── main.py ├── pdm.lock ├── piccolo_conf.py ├── piccolo_conf_test.py ├── pyproject.toml ├── sample.env ├── settings.py └── static ├── favicon.ico └── main.css /.build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12.2-slim-bullseye 2 | WORKDIR /opt/app 3 | ENV PYTHONDONTWRITEBYTECODE 1 4 | ENV PYTHONUNBUFFERED 1 5 | 6 | # Install required packages 7 | RUN apt-get update --allow-releaseinfo-change && \ 8 | apt-get update && \ 9 | apt-get upgrade -y 10 | RUN apt-get install -y git 11 | RUN apt-get install -y vim 12 | 13 | # Install project requirements 14 | COPY ["pyproject.toml", "pdm.lock", "/opt/app/"] 15 | RUN pip install --upgrade pip 16 | RUN pip install pdm 17 | RUN pdm venv create -f -n env 18 | RUN pdm sync --dev 19 | COPY . /opt/app/ 20 | EXPOSE 8000 21 | 22 | CMD exec /bin/sh -c "trap : TERM INT; (while true; do sleep 1000; done) & wait" 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | 13 | 14 | - package-ecosystem: 'github-actions' 15 | directory: '/' 16 | schedule: 17 | interval: 'daily' 18 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '26 2 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v3 71 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | services: 15 | postgres: 16 | image: postgres:14 17 | env: 18 | POSTGRES_PASSWORD: postgres 19 | options: >- 20 | --health-cmd pg_isready 21 | --health-interval 10s 22 | --health-timeout 5s 23 | --health-retries 5 24 | ports: 25 | - 5432:5432 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Setup env 30 | run: | 31 | rm sample.env 32 | touch sample.env 33 | echo DB_HOST=postgres >> sample.env 34 | echo DB_TEST_NAME=test >> sample.env 35 | echo DB_TEST_USER=postgres >> sample.env 36 | echo DB_TEST_PASSWORD=postgres >> sample.env 37 | echo DB_TEST_HOST=postgres >> sample.env 38 | echo DB_TEST_PORT=5432 >> sample.env 39 | 40 | - name: Build the Docker image 41 | run: docker build . --file .build/Dockerfile --tag fastapi-piccolo:dev 42 | # - name: Setup postgres 43 | # run: | 44 | # export PGPASSWORD=postgres 45 | # psql -h localhost -c 'CREATE DATABASE test;' -U postgres 46 | 47 | # - name: Run tests 48 | # run: docker run fastapi-piccolo:dev piccolo tester run 49 | 50 | # - name: Upload coverage 51 | # uses: codecov/codecov-action@v1 52 | 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | # Trigger the workflow on push or pull request, 5 | # but only for the main branch 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | run-linters: 15 | name: Run linters 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Check out Git repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up Python 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: 3.10.5 26 | 27 | - name: Install Python dependencies 28 | run: pip install black 29 | 30 | - name: Run linters 31 | uses: wearerequired/lint-action@v2 32 | with: 33 | black: true 34 | auto_fix: true 35 | -------------------------------------------------------------------------------- /.github/workflows/update_dep.yml: -------------------------------------------------------------------------------- 1 | name: Update dependencies 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | update-dependencies: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Update dependencies 15 | uses: pdm-project/update-deps-action@main 16 | with: 17 | # The personal access token, default: ${{ github.token }} 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | # The commit message" 20 | commit-message: "chore: Update pdm.lock" 21 | # The PR title 22 | pr-title: "Update dependencies" 23 | # The update strategy, can be 'reuse', 'eager' or 'all' 24 | update-strategy: eager 25 | # Whether to install PDM plugins before update 26 | install-plugins: "false" 27 | # Whether commit message contains signed-off-by 28 | sign-off-commit: "false" 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .idea 3 | .vscode 4 | .DS_Store 5 | .env.example 6 | /__pypackages__/ -------------------------------------------------------------------------------- /.pdm-python: -------------------------------------------------------------------------------- 1 | /home/runner/work/FastAPI-Piccolo-Template/FastAPI-Piccolo-Template/.venv/bin/python -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastAPI-Piccolo project template 2 | - Amazing [FastAPI](https://github.com/tiangolo/fastapi) framework. 3 | - Full Docker integration 4 | - Docker Compose integration and optimization for local development. 5 | - JWT token authentication. 6 | - Secure password hashing by default. 7 | - [Piccolo Admin](https://github.com/piccolo-orm/piccolo_admin) frontend 8 | - [Piccolo async ORM](https://github.com/piccolo-orm/piccolo) 9 | - simple CI/CD 10 | 11 | 12 | See each project's documentation for more information. 13 | 14 | 15 | ## Setup 16 | 17 | ### build the containers 18 | 19 | ```bash 20 | docker-compose build 21 | ``` 22 | ### run the server for local development 23 | 24 | ```bash 25 | docker-compose up 26 | ``` 27 | 28 | 29 | ### Running tests 30 | 31 | ```bash 32 | docker-compose run web sh -c "pdm run test" 33 | ``` 34 | 35 | 36 | ## todo: 37 | - [ ] add load balancer (Traefik) 38 | - [ ] add caching mechanism 39 | - [ ] add celery and flower for monitoring jobs 40 | - [X] add pgadmin 41 | - [ ] full CI/CD actions 42 | - [ ] Gunicorn integration for single server mode 43 | - [ ] Docker swarm integration + kubernetes for cluster mode 44 | - [ ] CookieCutter 45 | -------------------------------------------------------------------------------- /accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AliSayyah/FastAPI-Piccolo-Template/426399074fe3db98237fc30b18586fb677a1ab41/accounts/__init__.py -------------------------------------------------------------------------------- /accounts/endpoints.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from datetime import timedelta, datetime 3 | 4 | from fastapi import Depends, HTTPException, status, APIRouter 5 | from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm 6 | from jose import JWTError, jwt 7 | from piccolo.apps.user.tables import BaseUser 8 | 9 | from accounts.tables import TokenData, Token, UserModelOut, UserModelIn 10 | from settings import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES 11 | 12 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="accounts/login") 13 | accounts_router = APIRouter(prefix="/accounts") 14 | 15 | 16 | def create_access_token(data: dict, expires_delta: t.Optional[timedelta] = None): 17 | to_encode = data.copy() 18 | if expires_delta: 19 | expire = datetime.utcnow() + expires_delta 20 | else: 21 | expire = datetime.utcnow() + timedelta(minutes=15) 22 | to_encode.update({"exp": expire}) 23 | encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) 24 | return encoded_jwt 25 | 26 | 27 | async def get_current_user(token: str = Depends(oauth2_scheme)): 28 | credentials_exception = HTTPException( 29 | status_code=status.HTTP_401_UNAUTHORIZED, 30 | detail="Could not validate credentials", 31 | headers={"WWW-Authenticate": "Bearer"}, 32 | ) 33 | try: 34 | payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) 35 | username: str = payload.get("sub") 36 | if username is None: 37 | raise credentials_exception 38 | token_data = TokenData(username=username) 39 | except JWTError: 40 | raise credentials_exception 41 | user = ( 42 | await BaseUser.select() 43 | .where(BaseUser.username == token_data.username) 44 | .first() 45 | .run() 46 | ) 47 | if user is None: 48 | raise credentials_exception 49 | return user 50 | 51 | 52 | @accounts_router.post("/login/", response_model=Token, tags=["Auth"]) 53 | async def login_user( 54 | form_data: OAuth2PasswordRequestForm = Depends(), 55 | ): 56 | user = await BaseUser.login( 57 | username=form_data.username, password=form_data.password 58 | ) 59 | result = await BaseUser.select().where(BaseUser.id == user).first().run() 60 | if not user: 61 | raise HTTPException( 62 | status_code=status.HTTP_401_UNAUTHORIZED, 63 | detail="Incorrect username or password", 64 | headers={"WWW-Authenticate": "Bearer"}, 65 | ) 66 | access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) 67 | access_token = create_access_token( 68 | data={"sub": result["username"]}, expires_delta=access_token_expires 69 | ) 70 | return { 71 | "access_token": access_token, 72 | "token_type": "bearer", 73 | } 74 | 75 | 76 | @accounts_router.post("/register/", response_model=UserModelOut, tags=["Auth"]) 77 | async def register_user(user: UserModelIn): 78 | user = BaseUser(**user.__dict__) 79 | if ( 80 | await BaseUser.exists().where(BaseUser.email == str(user.email)).run() 81 | or await BaseUser.exists().where(BaseUser.username == str(user.username)).run() 82 | ): 83 | raise HTTPException( 84 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, 85 | detail="User with that email or username already exists.", 86 | ) 87 | await user.save().run() 88 | return UserModelOut(**user.__dict__) 89 | -------------------------------------------------------------------------------- /accounts/piccolo_app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Import all of the Tables subclasses in your app here, and register them with 3 | the APP_CONFIG. 4 | """ 5 | 6 | import os 7 | 8 | from piccolo.conf.apps import AppConfig 9 | 10 | 11 | CURRENT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) 12 | 13 | 14 | APP_CONFIG = AppConfig( 15 | app_name="accounts", 16 | migrations_folder_path=os.path.join(CURRENT_DIRECTORY, "piccolo_migrations"), 17 | table_classes=[], 18 | migration_dependencies=[], 19 | commands=[], 20 | ) 21 | -------------------------------------------------------------------------------- /accounts/piccolo_migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AliSayyah/FastAPI-Piccolo-Template/426399074fe3db98237fc30b18586fb677a1ab41/accounts/piccolo_migrations/__init__.py -------------------------------------------------------------------------------- /accounts/tables.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | from piccolo.apps.user.tables import BaseUser 4 | from piccolo.utils.pydantic import create_pydantic_model 5 | from pydantic import BaseModel 6 | 7 | 8 | # token schema 9 | class Token(BaseModel): 10 | access_token: str 11 | token_type: str 12 | 13 | 14 | class TokenData(BaseModel): 15 | username: t.Optional[str] = None 16 | 17 | 18 | UserModelIn = create_pydantic_model( 19 | table=BaseUser, 20 | model_name="UserModelIn", 21 | ) 22 | UserModelOut = create_pydantic_model( 23 | table=BaseUser, 24 | include_default_columns=True, 25 | model_name="UserModelOut", 26 | ) 27 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from piccolo.engine import engine_finder 4 | from piccolo_admin.endpoints import create_admin 5 | from starlette.routing import Route, Mount 6 | from starlette.staticfiles import StaticFiles 7 | 8 | from accounts.endpoints import accounts_router 9 | from home.endpoints import HomeEndpoint 10 | from home.piccolo_app import APP_CONFIG 11 | from settings import ORIGINS 12 | 13 | app = FastAPI( 14 | routes=[ 15 | Route("/", HomeEndpoint), 16 | Mount( 17 | "/admin/", 18 | create_admin( 19 | tables=APP_CONFIG.table_classes, 20 | # Required when running under HTTPS: 21 | # allowed_hosts=['my_site.com'] 22 | ), 23 | ), 24 | Mount("/static/", StaticFiles(directory="static")), 25 | ], 26 | ) 27 | 28 | app.add_middleware( 29 | CORSMiddleware, 30 | allow_origins=ORIGINS, 31 | allow_credentials=True, 32 | allow_methods=["*"], 33 | allow_headers=["*"], 34 | ) 35 | 36 | app.include_router(accounts_router) 37 | 38 | 39 | @app.on_event("startup") 40 | async def open_database_connection_pool(): 41 | try: 42 | engine = engine_finder() 43 | await engine.start_connection_pool() 44 | except Exception as e: 45 | print(f"Unable to connect to the database: {e}") 46 | 47 | 48 | @app.on_event("shutdown") 49 | async def close_database_connection_pool(): 50 | try: 51 | engine = engine_finder() 52 | await engine.close_connection_pool() 53 | except Exception as e: 54 | print(f"Unable to connect to the database: {e}") 55 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from piccolo.utils.warnings import colored_warning 5 | 6 | 7 | def pytest_configure(*args): 8 | if os.environ.get("PICCOLO_TEST_RUNNER") != "True": 9 | colored_warning( 10 | "\n\n" 11 | "We recommend running Piccolo tests using the " 12 | "`piccolo tester run` command, which wraps Pytest, and makes " 13 | "sure the test database is being used. " 14 | "To stop this warning, modify conftest.py." 15 | "\n\n" 16 | ) 17 | sys.exit(1) 18 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | volumes: 4 | psql_data: 5 | pgadmin_data: 6 | pdm_data: 7 | 8 | services: 9 | web: 10 | build: 11 | dockerfile: .build/Dockerfile 12 | context: . 13 | restart: unless-stopped 14 | image: fastapi-piccolo:dev 15 | volumes: 16 | - ./:/opt/app 17 | - pdm_data:/root/.local/share/pdm/venvs/app-jNE7dXAC-env:z 18 | ports: 19 | - "8000:8000" 20 | depends_on: 21 | - db 22 | command: bash -c 'while ! 2 | 3 |
4 | 5 | 6 |