├── {{cookiecutter.project_slug}} ├── ignore_files │ └── .keep ├── app │ ├── resources │ │ └── .keep │ ├── frontend │ │ ├── static │ │ │ ├── js │ │ │ │ └── .keep │ │ │ ├── css │ │ │ │ └── .keep │ │ │ ├── fonts │ │ │ │ └── .keep │ │ │ └── images │ │ │ │ └── .keep │ │ └── templates │ │ │ └── index.html │ ├── api │ │ ├── database │ │ │ ├── models │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ ├── token.py │ │ │ │ ├── user.py │ │ │ │ └── auth.py │ │ │ ├── __init__.py │ │ │ ├── execute │ │ │ │ ├── __init__.py │ │ │ │ ├── user.py │ │ │ │ └── base_execute.py │ │ │ ├── migrate │ │ │ │ ├── __init__.py │ │ │ │ └── init_super_user.py │ │ │ └── connect.py │ │ ├── __init__.py │ │ ├── helpers │ │ │ ├── utils.py │ │ │ ├── model2dict.py │ │ │ ├── __init__.py │ │ │ ├── download.py │ │ │ └── dict2model.py │ │ ├── errors │ │ │ ├── __init__.py │ │ │ ├── error_message.py │ │ │ ├── http_error.py │ │ │ └── validation_error.py │ │ ├── routes │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ ├── user_route.py │ │ │ └── authentication.py │ │ ├── responses │ │ │ ├── __init__.py │ │ │ ├── model2reponse.py │ │ │ └── base.py │ │ ├── serializers │ │ │ └── __init__.py │ │ └── services │ │ │ ├── __init__.py │ │ │ ├── user_service.py │ │ │ └── authentication_service.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_api │ │ │ ├── __init__.py │ │ │ ├── test_authentication.py │ │ │ └── test_user_information.py │ │ └── test_services │ │ │ └── __init__.py │ ├── __init__.py │ ├── ml │ │ ├── utils │ │ │ └── __init__.py │ │ ├── figures │ │ │ └── __init__.py │ │ ├── metrics │ │ │ └── __init__.py │ │ ├── trainers │ │ │ └── __init__.py │ │ ├── base_model │ │ │ └── __init__.py │ │ ├── data_loader │ │ │ └── __init__.py │ │ ├── preprocessing │ │ │ └── __init__.py │ │ └── __init__.py │ ├── core │ │ ├── __init__.py │ │ ├── constant.py │ │ ├── logging.py │ │ └── config.py │ ├── utils │ │ ├── test-cov-html │ │ ├── test │ │ ├── lint │ │ └── format │ ├── logger │ │ └── logger.py │ └── main.py ├── configs │ ├── install_cuda11.sh │ └── install_oracle.sh ├── .dockerignore ├── template.env ├── logging.ini ├── .github │ └── dependabot.yml ├── docker │ ├── Dockerfile_GPU │ └── Dockerfile ├── docker-compose.yml ├── setup.cfg ├── pyproject.toml ├── README.md ├── .gitignore └── .pylintrc ├── .gitignore ├── cookiecutter.json ├── docker-compose.yml ├── .github └── workflows │ ├── docker-image.yml │ └── codeql-analysis.yml ├── LICENSE ├── CONTRIBUTING.md └── README.md /{{cookiecutter.project_slug}}/ignore_files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/resources/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/frontend/static/js/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/configs/install_cuda11.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/frontend/static/css/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/frontend/static/fonts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/frontend/static/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/database/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/__init__.py: -------------------------------------------------------------------------------- 1 | """Main application.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/__init__.py: -------------------------------------------------------------------------------- 1 | """Define API.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/helpers/utils.py: -------------------------------------------------------------------------------- 1 | """Utils.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/ml/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Utils.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/ml/figures/__init__.py: -------------------------------------------------------------------------------- 1 | """Figures.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/ml/metrics/__init__.py: -------------------------------------------------------------------------------- 1 | """Metrics.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/ml/trainers/__init__.py: -------------------------------------------------------------------------------- 1 | """Trainers.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/errors/__init__.py: -------------------------------------------------------------------------------- 1 | """Error Handler.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/routes/__init__.py: -------------------------------------------------------------------------------- 1 | """Define all api.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/ml/base_model/__init__.py: -------------------------------------------------------------------------------- 1 | """Base model.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/ml/data_loader/__init__.py: -------------------------------------------------------------------------------- 1 | """Data Loader.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/tests/test_api/__init__.py: -------------------------------------------------------------------------------- 1 | """Test api.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/database/__init__.py: -------------------------------------------------------------------------------- 1 | """Database Models""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/responses/__init__.py: -------------------------------------------------------------------------------- 1 | """API Responses.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | """Serializers.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/ml/preprocessing/__init__.py: -------------------------------------------------------------------------------- 1 | """Preprocessing.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/core/__init__.py: -------------------------------------------------------------------------------- 1 | """Define config, constant, etc.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/tests/test_services/__init__.py: -------------------------------------------------------------------------------- 1 | """Test services.""" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | testing-project 3 | .mypy_cache 4 | poetry.lock 5 | dev-link/ 6 | .idea 7 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/database/execute/__init__.py: -------------------------------------------------------------------------------- 1 | """Execute Database.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/helpers/model2dict.py: -------------------------------------------------------------------------------- 1 | """Convert model to dictionary.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/database/migrate/__init__.py: -------------------------------------------------------------------------------- 1 | """Migrate data to database.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | """Contain general helper functions.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/helpers/download.py: -------------------------------------------------------------------------------- 1 | """Download file from the internet.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.dockerignore: -------------------------------------------------------------------------------- 1 | .venv 2 | venv 3 | .idea 4 | !app/resources/.keep 5 | app/resources/*/* 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/utils/test-cov-html: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | bash app/tests --cov-report=html -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/utils/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | pytest --cov=app --cov=app/tests --cov-report=term-missing --cov-config=setup.cfg -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/ml/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contain Machine learning model: build, train. 3 | ETL data. 4 | Define metrics, loss function. 5 | Other task like figures. 6 | """ 7 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/services/__init__.py: -------------------------------------------------------------------------------- 1 | """Define service for calling in api.""" 2 | from app.api.services.user_service import UserService 3 | 4 | user_service = UserService() -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/utils/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | 7 | flake8 app --exclude=app/db/migrations 8 | mypy app 9 | 10 | black --check app --diff 11 | isort --check-only app -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/frontend/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FastAPI 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/database/models/base.py: -------------------------------------------------------------------------------- 1 | """Custom base model.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pydantic import BaseModel 6 | 7 | 8 | class CustomBaseModel(BaseModel): 9 | """Custom Base Model.""" 10 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/routes/api.py: -------------------------------------------------------------------------------- 1 | """API Routes.""" 2 | from fastapi import APIRouter 3 | 4 | from app.api.routes import user_route 5 | 6 | app = APIRouter() 7 | 8 | app.include_router(user_route.router, tags=["User"], prefix="/admin/user") 9 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "Base Project", 3 | "project_slug": "{{ cookiecutter.project_name|lower|replace(' ', '_') }}", 4 | "host": "0.0.0.0", 5 | "port": "8088", 6 | "super_username": "admin", 7 | "super_password": "admin" 8 | } 9 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/utils/format: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | isort --force-single-line-imports app app/tests 7 | autoflake --recursive --remove-all-unused-imports --remove-unused-variables --in-place app app/tests 8 | isort app app/tests 9 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/errors/error_message.py: -------------------------------------------------------------------------------- 1 | """Error Messages.""" 2 | 3 | 4 | class BaseErrorMessage: 5 | """Base error message.""" 6 | status_code: int 7 | message_code: int 8 | message: str 9 | 10 | def __init__(self, *args): 11 | self.message = self.message.format(*args) 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | app: 5 | build: 6 | context: "{{cookiecutter.project_slug}}" 7 | dockerfile: "docker/Dockerfile" 8 | restart: on-failure 9 | depends_on: 10 | - mongodb 11 | ports: 12 | - "8088:8088" 13 | 14 | mongodb: 15 | image: mongo 16 | restart: always 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/template.env: -------------------------------------------------------------------------------- 1 | APP_HOST={{cookiecutter.host}} 2 | APP_PORT={{cookiecutter.port}} 3 | 4 | MONGO_DETAILS=mongodb://localhost:27017 5 | SECRET_KEY=09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7 6 | ALGORITHM=HS256 7 | 8 | SUPER_USERNAME={{cookiecutter.super_username}} 9 | SUPER_PASSWORD={{cookiecutter.super_password}} 10 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/helpers/dict2model.py: -------------------------------------------------------------------------------- 1 | """Convert dictionary to model.""" 2 | from app.api.database.models.user import UserSchema 3 | 4 | 5 | def convert_user_to_model(user_data: dict) -> UserSchema: 6 | """Convert user dictionary data to UserSchema.""" 7 | user_data["user_id"] = str(user_data.get("_id")) 8 | return UserSchema(**user_data) 9 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/database/models/token.py: -------------------------------------------------------------------------------- 1 | # skip-file 2 | """Token Models.""" 3 | from typing import Optional as Op 4 | 5 | from app.api.database.models.base import CustomBaseModel 6 | 7 | 8 | class Token(CustomBaseModel): 9 | """Token Model.""" 10 | access_token: str 11 | token_type: str 12 | 13 | 14 | class TokenData(CustomBaseModel): 15 | """Token Data.""" 16 | username: Op[str] = None 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/database/connect.py: -------------------------------------------------------------------------------- 1 | """Connect Oracle Database.""" 2 | import pymongo 3 | 4 | from app.core.constant import MONGO_DETAILS 5 | 6 | client = pymongo.MongoClient(MONGO_DETAILS, serverSelectionTimeoutMS=5000) 7 | 8 | database = client.test 9 | # database = client.{{ cookiecutter.project_slug}} 10 | 11 | user_collection = database.get_collection("user") 12 | user_information_collection = database.get_collection("user_information") 13 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/responses/model2reponse.py: -------------------------------------------------------------------------------- 1 | """Convert model to response.""" 2 | from app.api.database.models.user import UserSchema 3 | 4 | 5 | def convert_user_model_to_response(user: UserSchema): 6 | """Convert user model for response.""" 7 | return { 8 | "user_id": user.user_id, 9 | "username": user.username, 10 | "role": user.role, 11 | "createAt": user.created_at, 12 | "updateAt": user.updated_at, 13 | } 14 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/core/constant.py: -------------------------------------------------------------------------------- 1 | """Root constant define.""" 2 | import os 3 | 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | 8 | APP_HOST = os.getenv("APP_HOST") 9 | APP_PORT = os.getenv("APP_PORT") 10 | 11 | MONGO_DETAILS = os.getenv("MONGO_DETAILS") 12 | 13 | SECRET_KEY = os.getenv('SECRET_KEY') 14 | ALGORITHM = os.getenv('ALGORITHM') 15 | ACCESS_TOKEN_EXPIRE_MINUTES = 43200 16 | 17 | ADMIN_USER = -1 18 | SUPER_USERNAME = os.getenv('SUPER_USERNAME') 19 | SUPER_PASSWORD = os.getenv('SUPER_PASSWORD') 20 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/logging.ini: -------------------------------------------------------------------------------- 1 | [logging] 2 | script_location = ./app/logger 3 | 4 | [loggers] 5 | keys = root 6 | 7 | [handlers] 8 | keys = console 9 | 10 | [formatters] 11 | keys = generic 12 | 13 | [logger_root] 14 | level = WARN 15 | handlers = console 16 | qualname = 17 | 18 | [logger_logging] 19 | level = INFO 20 | handlers = 21 | qualname = logging 22 | 23 | [handler_console] 24 | class = StreamHandler 25 | args = (sys.stderr,) 26 | level = NOTSET 27 | formatter = generic 28 | 29 | [formatter_generic] 30 | format = %(levelname)-5.5s [%(name)s] %(message)s 31 | datefmt = %H:%M:%S -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/errors/http_error.py: -------------------------------------------------------------------------------- 1 | """HTTP Error handler""" 2 | 3 | from fastapi import HTTPException 4 | from starlette.requests import Request 5 | from starlette.responses import JSONResponse 6 | 7 | 8 | async def http_error_handler(request: Request, exc: HTTPException) -> JSONResponse: 9 | """HTTP Error handler. 10 | 11 | Args: 12 | request: Request. 13 | exc: Exception. 14 | 15 | Returns: 16 | Json response error. 17 | """ 18 | if request: 19 | pass 20 | 21 | return JSONResponse({"errors": [exc.detail]}, status_code=exc.status_code) 22 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.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/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | target-branch: "develop" 13 | assignees: 14 | - "khanh41" 15 | 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/services/user_service.py: -------------------------------------------------------------------------------- 1 | """User Service.""" 2 | from datetime import datetime 3 | 4 | from app.api.database.execute.user import user_execute 5 | from app.api.database.models.user import UserSchema 6 | from app.api.services import authentication_service 7 | 8 | 9 | class UserService: 10 | """User Service.""" 11 | 12 | @staticmethod 13 | def add_user(user: UserSchema): 14 | """Add user to database.""" 15 | user_password = user.password 16 | user.password = authentication_service.get_password_hash(user_password) 17 | user.created_at = datetime.now() 18 | user.updated_at = datetime.now() 19 | 20 | new_user = user_execute.add_data(user) 21 | new_user.password = user_password 22 | return new_user 23 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/database/models/user.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | """User Model.""" 3 | from datetime import datetime 4 | from typing import Optional 5 | 6 | from pydantic import Field 7 | 8 | from app.api.database.models.base import CustomBaseModel 9 | 10 | 11 | class UserSchema(CustomBaseModel): 12 | """User Schema.""" 13 | user_id: Optional[str] = None 14 | username: str = Field(...) 15 | password: str = Field(...) 16 | role: int = Field(...) 17 | created_at: Optional[datetime] = None 18 | updated_at: Optional[datetime] = None 19 | 20 | class Config: 21 | """Config.""" 22 | json_schema_extra = { 23 | "example": { 24 | "username": "example@gmail.com", 25 | "password": "abcd123456", 26 | "role": 0, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/tests/test_api/test_authentication.py: -------------------------------------------------------------------------------- 1 | """User Authentication Unit Test.""" 2 | from __future__ import annotations 3 | 4 | from fastapi.testclient import TestClient 5 | 6 | from app.main import app 7 | 8 | client = TestClient(app) 9 | 10 | 11 | def test_login_error(): 12 | """Test login error.""" 13 | response = client.get("/login") 14 | assert response.status_code == 401 15 | 16 | response = client.post("/token") 17 | assert response.status_code == 422 18 | 19 | response = client.post("/docs") 20 | assert response.status_code == 405 21 | 22 | response = client.get("/users/me") 23 | assert response.status_code == 403 24 | 25 | 26 | def test_logout_success(): 27 | """Test login error.""" 28 | response = client.get("") 29 | assert response.status_code == 200 30 | 31 | response = client.get("/logout") 32 | assert response.status_code == 200 33 | -------------------------------------------------------------------------------- /.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 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Create env 17 | env: 18 | MONGO_DETAILS: ${{ secrets.MONGO_DETAILS }} 19 | SECRET_KEY: ${{ secrets.SECRET_KEY }} 20 | ALGORITHM: ${{ secrets.ALGORITHM }} 21 | SOURCE_DIRECTORY: "{{cookiecutter.project_slug}}" 22 | run: | 23 | mv $SOURCE_DIRECTORY/template.env $SOURCE_DIRECTORY/.env 24 | sed -i 's/MONGO_DETAILS=.*/MONGO_DETAILS=$MONGO_DETAILS/' $SOURCE_DIRECTORY/.env 25 | sed -i 's/SECRET_KEY=.*/SECRET_KEY=$SECRET_KEY/' $SOURCE_DIRECTORY/.env 26 | sed -i 's/ALGORITHM=.*/ALGORITHM=$ALGORITHM/' $SOURCE_DIRECTORY/.env 27 | - name: Build the Docker image 28 | run: docker-compose up --build -d 29 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/core/logging.py: -------------------------------------------------------------------------------- 1 | """Logging.""" 2 | 3 | import logging 4 | from types import FrameType 5 | from typing import cast 6 | 7 | from loguru import logger 8 | 9 | 10 | class InterceptHandler(logging.Handler): 11 | """Intercept Handler.""" 12 | 13 | def emit(self, record: logging.LogRecord) -> None: # pragma: no cover 14 | """Emit.""" 15 | # Get corresponding Loguru level if it exists 16 | try: 17 | level = logger.level(record.levelname).name 18 | except ValueError: 19 | level = str(record.levelno) 20 | 21 | # Find caller from where originated the logged message 22 | frame, depth = logging.currentframe(), 2 23 | while frame.f_code.co_filename == logging.__file__: # noqa: WPS609 24 | frame = cast(FrameType, frame.f_back) 25 | depth += 1 26 | 27 | logger.opt(depth=depth, exception=record.exc_info).log( 28 | level, 29 | record.getMessage(), 30 | ) 31 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docker/Dockerfile_GPU: -------------------------------------------------------------------------------- 1 | FROM nvidia/cuda:11.0.3-base-ubuntu20.04 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | WORKDIR /app 6 | 7 | RUN apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/3bf863cc.pub 8 | 9 | RUN apt-get update && \ 10 | apt-get install gcc -y && \ 11 | apt-get install ffmpeg libsm6 libxext6 -y && \ 12 | apt-get install -y --no-install-recommends netcat && \ 13 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* .venv 14 | 15 | RUN apt-get update && \ 16 | apt-get install python3 python3-pip build-essential cmake pkg-config libx11-dev libatlas-base-dev libgtk-3-dev libboost-python-dev -y 17 | 18 | RUN pip install poetry && poetry config virtualenvs.in-project false 19 | 20 | COPY pyproject.toml ./ 21 | RUN poetry install --no-dev 22 | 23 | COPY . ./ 24 | 25 | RUN poetry run pytest -v 26 | 27 | CMD ["poetry", "run", "uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "{{cookiecutter.port}}"] 28 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/errors/validation_error.py: -------------------------------------------------------------------------------- 1 | """Validation Error.""" 2 | from __future__ import annotations 3 | 4 | from fastapi.exceptions import RequestValidationError 5 | from fastapi.openapi.constants import REF_PREFIX 6 | from fastapi.openapi.utils import validation_error_response_definition 7 | from pydantic import ValidationError 8 | from starlette.requests import Request 9 | from starlette.responses import JSONResponse 10 | from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY 11 | 12 | 13 | async def http422_error_handler( 14 | request: Request, 15 | exc: (RequestValidationError | ValidationError), 16 | ) -> JSONResponse: 17 | """HTTP 422 Error handler.""" 18 | return JSONResponse( 19 | {"errors": exc.errors()}, 20 | status_code=HTTP_422_UNPROCESSABLE_ENTITY, 21 | ) 22 | 23 | 24 | validation_error_response_definition["properties"] = { 25 | "errors": { 26 | "title": "Errors", 27 | "type": "array", 28 | "items": {"$ref": f"{REF_PREFIX}ValidationError"}, 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/configs/install_oracle.sh: -------------------------------------------------------------------------------- 1 | mkdir -p /opt/ora/ 2 | cd /opt/ora/ 3 | 4 | wget https://github.com/leonnleite/instant-client-oracle/raw/master/instantclient-basic-linux.x64-12.2.0.1.0.zip 5 | wget https://github.com/leonnleite/instant-client-oracle/raw/master/instantclient-sdk-linux.x64-12.2.0.1.0.zip 6 | 7 | unzip instantclient-basic-linux.x64-12.2.0.1.0.zip -d /opt/ora/ 8 | unzip instantclient-sdk-linux.x64-12.2.0.1.0.zip -d /opt/ora/ 9 | 10 | apt-get install -y libaio1 11 | rm -rf /etc/profile.d/oracle.sh 12 | echo "export ORACLE_HOME=/opt/ora/instantclient_12_2" >>/etc/profile.d/oracle.sh 13 | echo "export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$ORACLE_HOME" >>/etc/profile.d/oracle.sh 14 | chmod 777 /etc/profile.d/oracle.sh 15 | /bin/bash /etc/profile.d/oracle.sh 16 | env | grep -i ora # This will check current ENVIRONMENT settings for Oracle 17 | 18 | rm -rf /etc/ld.so.conf.d/oracle.conf 19 | echo "/opt/ora/instantclient_12_2" >>/etc/ld.so.conf.d/oracle.conf 20 | ldconfig 21 | cd $ORACLE_HOME 22 | 23 | ln -s libclntsh.so.12.1 libclntsh.so 24 | 25 | cd /app 26 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/database/execute/user.py: -------------------------------------------------------------------------------- 1 | """User Execute Define.""" 2 | from __future__ import annotations 3 | 4 | from app.api.database.connect import user_collection 5 | from app.api.database.execute.base_execute import BaseExecute 6 | from app.api.database.models.user import UserSchema 7 | from app.api.helpers.dict2model import convert_user_to_model 8 | 9 | 10 | class UserExecute(BaseExecute): 11 | """User Execute.""" 12 | 13 | def retrieve_data_by_username(self, username: str) -> UserSchema | None: 14 | """Get user information by a username.""" 15 | if data := self.data_collection.find_one({"username": username}): 16 | return self.convert_helper(data) 17 | return None 18 | 19 | def retrieve_data_by_role(self, role: int) -> UserSchema | None: 20 | """Get user information by a role.""" 21 | if data := self.data_collection.find_one({"role": role}): 22 | return self.convert_helper(data) 23 | return None 24 | 25 | 26 | user_execute = UserExecute(user_collection, convert_user_to_model) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Khánh Pluto 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/database/migrate/init_super_user.py: -------------------------------------------------------------------------------- 1 | """Init superuser.""" 2 | from __future__ import annotations 3 | 4 | import datetime 5 | from typing import Any 6 | 7 | from fastapi.encoders import jsonable_encoder 8 | 9 | from app.api.database.execute.user import user_execute as execute 10 | from app.api.helpers.dict2model import convert_user_to_model 11 | from app.api.services import authentication_service 12 | from app.core.constant import ADMIN_USER, SUPER_PASSWORD, SUPER_USERNAME 13 | 14 | 15 | def init_super_user(): 16 | """Init superuser if not exist.""" 17 | if not execute.retrieve_data_by_role(ADMIN_USER): 18 | print("Create Super user...") 19 | user: dict[str, Any] = jsonable_encoder({ 20 | "username": str(SUPER_USERNAME), 21 | "password": str(SUPER_PASSWORD), 22 | "role": -1, 23 | }) 24 | user['password'] = authentication_service.get_password_hash(user['password']) 25 | user['created_at'] = datetime.datetime.now() 26 | user['updated_at'] = datetime.datetime.now() 27 | 28 | execute.add_data(convert_user_to_model(user)) 29 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | backend-app: 5 | container_name: {{cookiecutter.project_slug}}-backend-app 6 | build: 7 | context: . 8 | dockerfile: docker/Dockerfile 9 | restart: always 10 | depends_on: 11 | - mongodb 12 | ports: 13 | - "{{cookiecutter.port}}:{{cookiecutter.port}}" 14 | healthcheck: 15 | test: [ "CMD-SHELL", "curl http://0.0.0.0:{{cookiecutter.port}}/" ] 16 | interval: 60s 17 | timeout: 5s 18 | retries: 5 19 | networks: 20 | - backend-network 21 | 22 | mongodb: 23 | image: mongo 24 | container_name: {{cookiecutter.project_slug}}-mongodb 25 | ports: 26 | - "27017:27017" 27 | restart: always 28 | healthcheck: 29 | test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet 30 | interval: 10s 31 | timeout: 5s 32 | retries: 5 33 | env_file: 34 | - .env 35 | volumes: 36 | - mongodb-volume:/data/db 37 | networks: 38 | - backend-network 39 | 40 | volumes: 41 | mongodb-volume: 42 | 43 | networks: 44 | backend-network: 45 | name: backend-network 46 | driver: bridge -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/core/config.py: -------------------------------------------------------------------------------- 1 | """Define a config for project.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | import sys 6 | 7 | from dotenv import load_dotenv 8 | from loguru import logger 9 | from starlette.config import Config 10 | from starlette.datastructures import CommaSeparatedStrings 11 | 12 | from app.core.logging import InterceptHandler 13 | 14 | load_dotenv() 15 | 16 | API_PREFIX = "/api" 17 | 18 | VERSION = "0.0.0" 19 | 20 | config = Config(".env") 21 | 22 | DEBUG: bool = config("DEBUG", cast=bool, default=False) 23 | 24 | PROJECT_NAME: str = config("{{ cookiecutter.project_name|replace(' ', '-') }}", 25 | default="{{ cookiecutter.project_name }} application") 26 | ALLOWED_HOSTS = config( 27 | "ALLOWED_HOSTS", 28 | cast=CommaSeparatedStrings, 29 | default="", 30 | ) 31 | 32 | # logging configuration 33 | 34 | LOGGING_LEVEL = logging.DEBUG if DEBUG else logging.INFO 35 | LOGGERS = ("uvicorn.asgi", "uvicorn.access") 36 | 37 | logging.getLogger().handlers = [InterceptHandler()] 38 | for logger_name in LOGGERS: 39 | logging_logger = logging.getLogger(logger_name) 40 | logging_logger.handlers = [InterceptHandler(level=LOGGING_LEVEL)] 41 | 42 | logger.configure(handlers=[{"sink": sys.stderr, "level": LOGGING_LEVEL}]) 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | This file explains the guidelines for making contributions to this repository. For easy merges, follow these guidelines. 3 | 4 | ## Ground Rules 5 | Before you make PRs to this repo: 6 | 7 | 1. Ensure that you are registered for HacktoberFest 2022 as a contributor. 8 | 2. You have read these guidelines thoroughly. 9 | 3. Your contribution is genuine and not another spam PR. 10 | 11 | ## How to make a PR? 12 | Follow these steps to make a successful PR: 13 | 14 | 1. Fork this repo and create a branch. 15 | 2. Add your contribution as a folder in the appropriate folder. If there is no-prexisting folder for your contribution, feel free to create one. 16 | 3. Merge your branch to main and PR to this repo. 17 | 4. In your PR description, be sure to include screenshots that prove that your code works. You are also encouraged to give a small description of your code / doc. 18 | 19 | **NOTE:** If you are making a PR for an open issue, first comment and ask the maintainers to assign it to you. 20 | 21 | ## Reporting bugs 22 | If you see a bug, you can report create an issue to report it. You may also fix the bug and raise a PR. 23 | 24 | ## How the PR gets merged? 25 | The PR will be reviewed by the maintainers of this repo and merged if deemed appropriate. You will receive a notification about the status of your PR. 26 | 27 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/tests/test_api/test_user_information.py: -------------------------------------------------------------------------------- 1 | """User Information Unit Test.""" 2 | from __future__ import annotations 3 | 4 | import json 5 | from typing import Any 6 | 7 | from fastapi.testclient import TestClient 8 | 9 | from app.main import app 10 | 11 | client = TestClient(app) 12 | 13 | with open("app/tests/example_data/user_information_request.json", 'r', encoding='UTF-8') as f: 14 | user_information_request: dict[str, Any] = json.load(f) 15 | 16 | 17 | def test_get_all_user_information(): 18 | """Test get all user information success.""" 19 | response = client.get("/api/admin/user") 20 | assert response.status_code == 200 21 | 22 | 23 | def test_rest_api_user_information(): 24 | """Test add and get user information success.""" 25 | response = client.post("/api/admin/user", json=user_information_request) 26 | assert response.status_code == 200 27 | 28 | user_id = response.json()["data"]["user_id"] 29 | 30 | response = client.put(f"/api/admin/user/{user_id}", json=user_information_request) 31 | assert response.status_code == 200 32 | 33 | response = client.get(f"/api/admin/user/{user_id}") 34 | assert response.status_code == 200 35 | 36 | response = client.delete(f"/api/admin/user/{user_id}") 37 | assert response.status_code == 200 38 | 39 | 40 | if __name__ == '__main__': 41 | test_rest_api_user_information() 42 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/responses/base.py: -------------------------------------------------------------------------------- 1 | """Base Response.""" 2 | 3 | from fastapi.responses import JSONResponse, ORJSONResponse 4 | from starlette import status 5 | 6 | 7 | class BaseResponse: 8 | """Base Response.""" 9 | 10 | @staticmethod 11 | def success_response( 12 | message: str = "SUCCESS", 13 | status_code: int = status.HTTP_200_OK, 14 | message_code: int = 0, 15 | data=None, 16 | ): 17 | """Success response with a message and status code.""" 18 | if data is None: 19 | return JSONResponse( 20 | status_code=status_code, 21 | content={ 22 | "message_code": message_code, 23 | "message": message, 24 | }, 25 | ) 26 | else: 27 | return ORJSONResponse( 28 | status_code=status_code, 29 | content={ 30 | "message_code": message_code, 31 | "message": message, 32 | "data": data, 33 | }, 34 | ) 35 | 36 | @staticmethod 37 | def error_response(message: str = "ERROR", status_code: int = status.HTTP_400_BAD_REQUEST, message_code: int = 0): 38 | """Error response with a message and status code.""" 39 | return JSONResponse( 40 | status_code=status_code, 41 | content={ 42 | "message_code": message_code, 43 | "message": message, 44 | } 45 | ) 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastAPI and Mongodb - Base Project Generator 🔥 2 | ![docker build](https://github.com/khanh41/fastapi-mongodb-base-project/actions/workflows/docker-image.yml/badge.svg) 3 | ![codeQL](https://github.com/khanh41/fastapi-mongodb-base-project/actions/workflows/codeql-analysis.yml/badge.svg) 4 | 5 | ## How to use it ❓ 6 | Go to the directory where you want to create your project and run: 7 | ```bash 8 | pip install cookiecutter 9 | cookiecutter https://github.com/khanh41/fastapi-mongodb-base-project 10 | ``` 11 | 12 | ### Generate passwords ⛏ 13 | You will be asked to provide passwords and secret keys for several components. Open another terminal and run: 14 | ```bash 15 | openssl rand -hex 32 16 | # Outputs something like: 99d3b1f01aa639e4a76f4fc281fc834747a543720ba4c8a8648ba755aef9be7f 17 | ``` 18 | 19 | Copy the contents and use that as password / secret key. And run that again to generate another secure key. 20 | 21 | 22 | ### Input variables 💬 23 | The generator (cookiecutter) will ask you for some data, you might want to have at hand before generating the project. 24 | 25 | The input variables, with their default values (some auto generated) are: 26 | 27 | * `project_name`: The name of the project 28 | * `project_slug`: The development friendly name of the project. By default, based on the project name 29 | * `host`: IP host running 30 | * `port`: Port running 31 | * `super_username`: The first superuser generated, with it you will be able to create more users, etc. By default, based on the domain. 32 | * `super_password`: First superuser password. Use the method above to generate it. 33 | 34 | ## License 💂 35 | 36 | This project is licensed under the terms of the MIT license. 37 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11.6 as python-base 2 | 3 | ENV PYTHONUNBUFFERED=1 \ 4 | PYTHONDONTWRITEBYTECODE=1 \ 5 | PIP_NO_CACHE_DIR=off \ 6 | PIP_DISABLE_PIP_VERSION_CHECK=on \ 7 | PIP_DEFAULT_TIMEOUT=100 \ 8 | POETRY_VERSION=1.6.1 \ 9 | POETRY_HOME="/opt/poetry" \ 10 | POETRY_VIRTUALENVS_IN_PROJECT=true \ 11 | POETRY_NO_INTERACTION=1 \ 12 | PYSETUP_PATH="/opt/pysetup" \ 13 | VENV_PATH="/opt/pysetup/.venv" 14 | 15 | ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH" 16 | 17 | RUN apt-get update && \ 18 | apt-get install --no-install-recommends -y curl build-essential 19 | 20 | FROM python-base as builder-base 21 | 22 | RUN curl -sSL https://install.python-poetry.org | python 23 | 24 | WORKDIR $PYSETUP_PATH 25 | COPY pyproject.toml ./ 26 | 27 | RUN poetry install 28 | 29 | FROM python-base as development 30 | ENV FASTAPI_ENV=development 31 | 32 | EXPOSE 8088 33 | 34 | WORKDIR $PYSETUP_PATH 35 | 36 | COPY --from=builder-base $POETRY_HOME $POETRY_HOME 37 | COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH 38 | 39 | COPY . /app/ 40 | WORKDIR /app 41 | 42 | RUN poetry run pytest -v 43 | 44 | CMD ["hypercorn", "app.main:app", "--worker-class", "trio", "--bind", "0.0.0.0:8088", "--workers", "4"] 45 | 46 | FROM python-base as production 47 | ENV FASTAPI_ENV=production 48 | 49 | EXPOSE 8088 50 | 51 | WORKDIR $PYSETUP_PATH 52 | 53 | COPY --from=builder-base $POETRY_HOME $POETRY_HOME 54 | COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH 55 | 56 | COPY . /app/ 57 | WORKDIR /app 58 | 59 | RUN poetry run pytest -v 60 | 61 | CMD ["hypercorn", "app.main:app", "--worker-class", "trio", "--bind", "0.0.0.0:{{cookiecutter.port}}", "--workers", "8"] 62 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/database/execute/base_execute.py: -------------------------------------------------------------------------------- 1 | """Base Execute.""" 2 | from __future__ import annotations 3 | 4 | from bson.objectid import ObjectId 5 | 6 | from app.api.database.models.base import CustomBaseModel 7 | 8 | 9 | class BaseExecute: 10 | """Base Execute.""" 11 | 12 | def __init__(self, data_collection, convert_helper): 13 | self.data_collection = data_collection 14 | self.convert_helper = convert_helper 15 | 16 | def retrieve_datas(self) -> list[CustomBaseModel]: 17 | """Retrieve all data.""" 18 | return [self.convert_helper(data) for data in self.data_collection.find()] 19 | 20 | def add_data(self, data: CustomBaseModel) -> CustomBaseModel: 21 | """Add a new data.""" 22 | data = self.data_collection.insert_one(data.model_dump()) 23 | new_data = self.data_collection.find_one({"_id": data.inserted_id}) 24 | return self.convert_helper(new_data) 25 | 26 | def retrieve_data(self, object_id: str) -> CustomBaseModel | None: 27 | """Retrieve data with a matching ID.""" 28 | if data := self.data_collection.find_one({"_id": ObjectId(object_id)}): 29 | return self.convert_helper(data) 30 | return None 31 | 32 | def update_data(self, object_id: str, update_data: dict) -> bool: 33 | """Update a data with a matching ID.""" 34 | if self.data_collection.find_one({"_id": ObjectId(object_id)}): 35 | updated_data = self.data_collection.update_one({"_id": ObjectId(object_id)}, {"$set": update_data}) 36 | return bool(updated_data) 37 | return False 38 | 39 | def delete_data(self, object_id: str) -> bool: 40 | """Delete a data.""" 41 | if self.data_collection.find_one({"_id": ObjectId(object_id)}): 42 | self.data_collection.delete_one({"_id": ObjectId(object_id)}) 43 | return True 44 | return False 45 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/setup.cfg: -------------------------------------------------------------------------------- 1 | [coverage:report] 2 | precision = 2 3 | exclude_lines = 4 | pragma: no cover 5 | raise NotImplementedError 6 | raise NotImplemented 7 | 8 | [coverage:run] 9 | source = app 10 | branch = True 11 | 12 | [mypy-loguru] 13 | ignore_missing_imports = True 14 | 15 | [mypy-asyncpg.*] 16 | ignore_missing_imports = True 17 | 18 | [mypy-bcrypt] 19 | ignore_missing_imports = True 20 | 21 | [mypy-passlib.*] 22 | ignore_missing_imports = True 23 | 24 | [mypy-slugify.*] 25 | ignore_missing_imports = True 26 | 27 | [mypy-pypika.*] 28 | ignore_missing_imports = True 29 | 30 | [flake8] 31 | format = wemake 32 | max-line-length = 120 33 | ignore = 34 | # common errors: 35 | # FastAPI architecture requires a lot of functions calls as default arguments, so ignore it here. 36 | B008, 37 | I001, I005, WPS318 38 | # docs are missing in this project. 39 | # D, RST 40 | 41 | # Found too short name 42 | WPS111, 43 | 44 | # Found redundant subscript slice 45 | WPS349, 46 | 47 | # Found usage of a variable marked as unused 48 | WPS121, WPS122, 49 | 50 | # Found using `@staticmethod` 51 | WPS602, 52 | 53 | # Jones Complexity 54 | WPS221, WPS210, WPS612, WPS605, WPS234, WPS220, 55 | 56 | # Found protected attribute usage 57 | WPS437, WPS323, WPS115, 58 | 59 | # WPS: 3xx 60 | # IMO, but the obligation to specify the base class is redundant. 61 | WPS306, 62 | 63 | # WPS: 4xx 64 | # FastAPI architecture requires a lot of complex calls as default arguments, so ignore it here. 65 | WPS404, 66 | # again, FastAPI DI architecture involves a lot of nested functions as DI providers. 67 | WPS430, 68 | # used for pypika operations 69 | WPS465, 70 | 71 | # WPS: 6xx 72 | # pydantic defines models in dataclasses model style, but not supported by WPS. 73 | # WPS601, 74 | no-accept-encodings = True 75 | nested-classes-whitelist=Config 76 | inline-quotes = double 77 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "app" 3 | version = "0.0.1" 4 | description = "" 5 | authors = ["Khánh Pluto"] 6 | license = "Khánh Pluto" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | pylint = "^3.0.2" 11 | python-slugify = "^8.0.1" 12 | Unidecode = "^1.3.7" 13 | loguru = "^0.7.2" 14 | pymongo = { extras = ["srv"], version = "^4.5.0" } 15 | aiofiles = "^23.2.1" 16 | PyYAML = "^6.0.1" 17 | python-dotenv = "^1.0.0" 18 | wget = "^3.2" 19 | passlib = { extras = ["bcrypt"], version = "^1.7.4" } 20 | python-jose = { extras = ["cryptography"], version = "^3.3.0" } 21 | Jinja2 = "^3.1.2" 22 | pytest = "^7.4.3" 23 | pytest-cov = "^4.1.0" 24 | pytest-asyncio = "^0.21.1" 25 | pytest-env = "^1.1.0" 26 | httpx = "^0.25.0" 27 | orjson = "^3.9.10" 28 | aiohttp = "^3.8.6" 29 | pytest-sugar = "^0.9.7" 30 | hypercorn = { extras = ["trio"], version = "^0.15.0" } 31 | gunicorn = "^21.2.0" 32 | python-multipart = "^0.0.6" 33 | fastapi = "^0.104.0" 34 | uvicorn = "^0.23.2" 35 | poetry-plugin-up = "^0.7.0" 36 | pydantic = "^2.4.2" 37 | 38 | [tool.poetry.dev-dependencies] 39 | black = "^23.10.1" 40 | isort = "^5.12.0" 41 | pyflakes = "3.1.0" 42 | flake8 = "6.1.0" 43 | wemake-python-styleguide = "^0.18.0" 44 | autoflake = "2.2.1" 45 | mypy = "^1.6.1" 46 | flake8-fixme = "^1.1.1" 47 | docker = "^6.1.3" 48 | asgi-lifespan = "^2.1.0" 49 | autopep8 = "2.0.4" 50 | 51 | [tool.pylint] 52 | line-length = 120 53 | 54 | [tool.isort] 55 | profile = "black" 56 | src_paths = ["app", "tests"] 57 | combine_as_imports = true 58 | 59 | [tool.pytest.ini_options] 60 | testpaths = "app/tests" 61 | filterwarnings = ''' 62 | ignore::DeprecationWarning 63 | ''' 64 | addopts = ''' 65 | --strict-markers 66 | --tb=short 67 | --cov=app 68 | --cov=tests 69 | --cov-branch 70 | --cov-report=term-missing 71 | --cov-report=html 72 | --cov-report=xml 73 | --no-cov-on-fail 74 | --cov-fail-under=70 75 | ''' 76 | env = ["SECRET_KEY=secret"] 77 | 78 | [build-system] 79 | requires = ["poetry>=1.0"] 80 | build-backend = "poetry.masonry.api" 81 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/routes/user_route.py: -------------------------------------------------------------------------------- 1 | """User route.""" 2 | from fastapi import APIRouter, Body 3 | 4 | from app.api.database.execute.user import user_execute as execute 5 | from app.api.database.models.user import UserSchema 6 | from app.api.responses.base import BaseResponse 7 | from app.api.responses.model2reponse import convert_user_model_to_response 8 | from app.api.services import user_service 9 | 10 | router = APIRouter() 11 | 12 | 13 | @router.post("", response_description="user data added into the database") 14 | async def add_user_data(user: UserSchema = Body(...)): 15 | """Add user data to database.""" 16 | return BaseResponse.success_response(data=user_service.add_user(user)) 17 | 18 | 19 | @router.get("", response_description="users retrieved") 20 | async def get_users(): 21 | """Get all user's information.""" 22 | users = execute.retrieve_datas() 23 | return BaseResponse.success_response(data=[convert_user_model_to_response(user) for user in users]) 24 | 25 | 26 | @router.get("/{user_id}", response_description="user data retrieved") 27 | async def get_user_data(user_id: str): 28 | """Get user information by id.""" 29 | if user := execute.retrieve_data(user_id): 30 | return BaseResponse.success_response(data=convert_user_model_to_response(user)) 31 | return BaseResponse.error_response(f"user {user_id} doesn't exist.", 404) 32 | 33 | 34 | @router.put("/{user_id}") 35 | async def update_user_data(user_id: str, req: UserSchema = Body(...)): 36 | """Update user data.""" 37 | req = {k: v for k, v in req.dict().items() if v is not None} 38 | if updated_user := execute.update_data(user_id, req): 39 | return BaseResponse.success_response(message=f"successful user update with ID {user_id}") 40 | return BaseResponse.error_response("There was an error updating the user data.", 404) 41 | 42 | 43 | @router.delete("/{user_id}", response_description="user data deleted from the database") 44 | async def delete_user_data(user_id: str): 45 | """Delete user data by user id.""" 46 | if deleted_user := execute.delete_data(user_id): 47 | return BaseResponse.success_response(message=f"user with ID: {user_id} removed") 48 | return BaseResponse.error_response(f"user with id {user_id} doesn't exist", 404) 49 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/logger/logger.py: -------------------------------------------------------------------------------- 1 | """Define Logger for log message.""" 2 | 3 | import logging 4 | from datetime import datetime 5 | 6 | from app.core.config import LOGGING_LEVEL 7 | 8 | 9 | class UvicornFormatter(logging.Formatter): 10 | """Uvicorn Formatter.""" 11 | 12 | FORMAT = ( 13 | "\033[38;5;244m%(asctime)s\033[0m" 14 | " | " 15 | "%(levelname)-7s" 16 | " | " 17 | "\033[38;5;214m%(name)s\033[0m" 18 | " : " 19 | "\033[38;5;111m%(message)s\033[0m" 20 | ) 21 | 22 | LEVEL_COLORS = { 23 | "DEBUG": "\033[38;5;32m", 24 | "INFO": "\033[38;5;36m", 25 | "WARNING": "\033[38;5;221m", 26 | "ERROR": "\033[38;5;196m", 27 | "CRITICAL": "\033[48;5;196;38;5;231m", 28 | } 29 | 30 | def format(self, record): 31 | """Config format""" 32 | levelname = record.levelname 33 | level_color = self.LEVEL_COLORS.get(levelname, "") 34 | record.levelname = f"{level_color}{levelname}\033[0m" 35 | record.asctime = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S.%f") 36 | return super().format(record) 37 | 38 | 39 | def configure_logging(): 40 | """Initialize logging defaults for Project. 41 | 42 | This function does: 43 | - Assign INFO and DEBUG level to logger file handler and console handler. 44 | 45 | Returns: 46 | Logger. 47 | """ 48 | logger = logging.getLogger() 49 | logger.setLevel(LOGGING_LEVEL) 50 | 51 | # Create a file handler with a lower log level 52 | file_handler = logging.FileHandler("app/logger/logger.log") 53 | file_handler.setLevel(LOGGING_LEVEL) 54 | 55 | # Create a console handler with a higher log level 56 | console_handler = logging.StreamHandler() 57 | console_handler.setLevel(LOGGING_LEVEL) 58 | 59 | # Create a formatter and add it to the handlers 60 | default_formatter = logging.Formatter( 61 | "[%(asctime)s] [%(levelname)s] [%(name)s] " 62 | "[%(funcName)s():%(lineno)s] [PID:%(process)d TID:%(thread)d] %(message)s", 63 | "%d/%m/%Y %H:%M:%S", 64 | ) 65 | 66 | file_handler.setFormatter(default_formatter) 67 | console_handler.setFormatter(UvicornFormatter(UvicornFormatter.FORMAT)) 68 | 69 | if logger.hasHandlers(): 70 | logger.handlers.clear() 71 | 72 | logger.addHandler(console_handler) 73 | logger.addHandler(file_handler) 74 | 75 | return logger 76 | 77 | 78 | custom_logger = configure_logging() 79 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | 2 | 3 | # For most projects, this workflow file will not need changing; you simply need 4 | # to commit it to your repository. 5 | # 6 | # You may wish to alter this file to override the set of languages analyzed, 7 | # or to provide custom queries or build logic. 8 | # 9 | # ******** NOTE ******** 10 | # We have attempted to detect the languages in your repository. Please check 11 | # the `language` matrix defined below to confirm you have the correct set of 12 | # supported CodeQL languages. 13 | # 14 | name: "CodeQL" 15 | 16 | on: 17 | push: 18 | branches: [ master ] 19 | pull_request: 20 | # The branches below must be a subset of the branches above 21 | branches: [ master ] 22 | schedule: 23 | - cron: '37 22 * * 5' 24 | 25 | jobs: 26 | analyze: 27 | name: Analyze 28 | runs-on: ubuntu-latest 29 | permissions: 30 | actions: read 31 | contents: read 32 | security-events: write 33 | 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | language: [ 'python' ] 38 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 39 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v2 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v1 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@v1 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 https://git.io/JvXDl 62 | 63 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 64 | # and modify them (or add more) to build your code if your project 65 | # uses a compiled language 66 | 67 | #- run: | 68 | # make bootstrap 69 | # make release 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v1 73 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/services/authentication_service.py: -------------------------------------------------------------------------------- 1 | """Authentication service.""" 2 | 3 | from __future__ import annotations 4 | 5 | from datetime import datetime, timedelta, timezone 6 | from typing import Optional as Op 7 | 8 | from fastapi import Depends, HTTPException, status 9 | from jose import JWTError, jwt 10 | 11 | from app.api.database.execute.user import user_execute as execute 12 | from app.api.database.models.auth import oauth2_scheme, pwd_context 13 | from app.api.database.models.token import TokenData 14 | from app.api.database.models.user import UserSchema 15 | from app.core.constant import ALGORITHM, SECRET_KEY 16 | 17 | 18 | async def get_current_user(token: str = Depends(oauth2_scheme)) -> dict: 19 | """Check login token of current user.""" 20 | credentials_exception = HTTPException( 21 | status_code=status.HTTP_401_UNAUTHORIZED, 22 | detail="Could not validate credentials", 23 | headers={"WWW-Authenticate": "Bearer"}, 24 | ) 25 | try: 26 | payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) 27 | username: str = payload.get("sub") 28 | if username is None: 29 | raise credentials_exception 30 | token_data = TokenData(username=username) 31 | except JWTError as exc: 32 | raise credentials_exception from exc 33 | 34 | user = execute.retrieve_data_by_username(username=token_data.username) 35 | if user is None: 36 | raise credentials_exception 37 | return user 38 | 39 | 40 | def verify_password(plain_password: str, hashed_password: str) -> bool: 41 | """Verify password.""" 42 | return pwd_context.verify(plain_password, hashed_password) 43 | 44 | 45 | def get_password_hash(password: str) -> str: 46 | """Password to hash password""" 47 | return pwd_context.hash(password) 48 | 49 | 50 | def authenticate_user(username: str, password: str) -> bool | UserSchema: 51 | """Authenticate user.""" 52 | if user := execute.retrieve_data_by_username(username): 53 | return user if verify_password(password, user.password) else False 54 | else: 55 | return False 56 | 57 | 58 | def create_access_token(data: dict, expires_delta: Op[timedelta] = None) -> str: 59 | """Create access token.""" 60 | to_encode = data.copy() 61 | if expires_delta: 62 | expire = datetime.now(timezone.utc) + expires_delta 63 | else: 64 | expire = datetime.now(timezone.utc) + timedelta(minutes=15) 65 | to_encode["exp"] = expire 66 | encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) 67 | return encoded_jwt 68 | 69 | 70 | async def get_current_active_user(current_user: UserSchema = Depends(get_current_user)) -> UserSchema: 71 | """Get current active user.""" 72 | return current_user 73 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/README.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.project_name}} 2 | 3 | ## Installation ⚡️ 4 | ### Requires 5 | - Python 3.10 6 | 7 | Install with poetry: 8 | ~~~ 9 | pip install poetry 10 | poetry install 11 | ~~~ 12 | 13 | Create `.env` file as `template.env` 14 | 15 | **WARNING**: 16 | - When run application in local: `MONGO_DETAILS=mongodb://localhost:27017` 17 | - When run application with docker-compose (not yet public port): `MONGO_DETAILS=mongodb://mongodb:27017` 18 | 19 | ## Deployment app ⛄️ 20 | Run MongoDB with docker: 21 | ~~~ 22 | sudo docker pull mongo 23 | sudo docker run --name some-mongo -p 27017:27017 -d mongo 24 | ~~~ 25 | 26 | ## Deployment with Docker 🐳 27 | Docker build and run with Dockerfile: 28 | ~~~ 29 | sudo docker pull mongo 30 | sudo docker run --name some-mongo -p 27017:27017 -d mongo 31 | sudo docker build -t {{cookiecutter.project_slug}}_image . 32 | sudo docker run -it -d {{cookiecutter.project_slug}}_container 33 | ~~~ 34 | Deployment with docker-compose: 35 | ~~~ 36 | docker-compose up -d 37 | ~~~ 38 | - Server backend - docs: http://{{cookiecutter.host}}:{{cookiecutter.port}}/docs 39 | - Api backend: http://{{cookiecutter.host}}:{{cookiecutter.port}}/redoc 40 | - Server frontend: http://{{cookiecutter.host}}:{{cookiecutter.port}} 41 | 42 | ## Run tests 😋 43 | Tests for this project are defined in the `tests/` folder. 44 | ~~~ 45 | poetry run pytest 46 | ~~~ 47 | 48 | ## Run tools 🌍 49 | Auto format: 50 | ~~~ 51 | poetry run app/utils/format 52 | ~~~ 53 | 54 | Auto lint: 55 | ~~~ 56 | poetry run pylint app 57 | # OR 58 | poetry run app/utils/lint 59 | ~~~ 60 | 61 | Auto test: 62 | ~~~ 63 | poetry run app/utils/test 64 | ~~~ 65 | 66 | ## Tree directory 🌗 67 | ~~~ 68 | app 69 | ├── api - web related stuff. 70 | │ ├── database - config database. 71 | │ │ ├── models - definition of table model. 72 | │ │ ├── excute - sql handling, CRUD. 73 | │ │ ├── connect.py - connect to database. 74 | │ ├── errors - definition of error handlers. 75 | │ └── routes - web routes. 76 | │ └── services - logic that is not just crud related. 77 | │ └── responses - response for api request corresponding. 78 | ├── ml - machine learning model and preprocessing. 79 | │ ├── data_loader - load data or model. 80 | │ ├── preprocessing - preprocessing data. 81 | │ ├── figures - draw (ignore). 82 | │ ├── metrics - metrics for model, etc. 83 | │ ├── base_model - model machine learning setup 84 | │ ├── trainers - model machine learning training. 85 | ├── core - application configuration, startup events, logging. 86 | ├── logger - export log for server process. 87 | ├── tests - test api, code. 88 | ├── utils - tools format, lint, test, etc. 89 | ├── resources - image, audio, csv, etc. (ignore) 90 | ├── pyproject.toml - dependencies and package. 91 | └── main.py - FastAPI application creation and configuration. 92 | ~~~ 93 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/database/models/auth.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | """Authentication Model.""" 3 | from typing import Optional 4 | 5 | from fastapi import HTTPException 6 | from fastapi.openapi.models import ( 7 | OAuthFlows as OAuthFlowsModel, 8 | SecurityBase as SecurityBaseModel, 9 | ) 10 | from fastapi.security import OAuth2 11 | from fastapi.security.base import SecurityBase 12 | from fastapi.security.utils import get_authorization_scheme_param 13 | from passlib.context import CryptContext 14 | from starlette.requests import Request 15 | from starlette.status import HTTP_403_FORBIDDEN 16 | 17 | 18 | class OAuth2PasswordBearerCookie(OAuth2): 19 | """Authentication Bearer Cookie.""" 20 | 21 | def __init__( 22 | self, 23 | token_url: str, 24 | scheme_name: str = None, 25 | scopes: dict = None, 26 | auto_error: bool = True, 27 | ) -> None: 28 | if not scopes: 29 | scopes = {} 30 | flows = OAuthFlowsModel(password={"tokenUrl": token_url, "scopes": scopes}) 31 | super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error) 32 | 33 | async def __call__(self, request: Request) -> Optional[str]: 34 | header_authorization: str = request.headers.get("Authorization") 35 | cookie_authorization: str = request.cookies.get("Authorization") 36 | 37 | header_scheme, header_param = get_authorization_scheme_param( 38 | header_authorization 39 | ) 40 | cookie_scheme, cookie_param = get_authorization_scheme_param( 41 | cookie_authorization 42 | ) 43 | 44 | if header_scheme.lower() == "bearer" and header_param != 'undefined': 45 | authorization = True 46 | scheme = header_scheme 47 | param = header_param 48 | elif cookie_scheme.lower() == "bearer" and cookie_param != 'undefined': 49 | authorization = True 50 | scheme = cookie_scheme 51 | param = cookie_param 52 | else: 53 | authorization = False 54 | scheme = "" 55 | param = "" 56 | 57 | if not authorization or scheme.lower() != "bearer": 58 | if self.auto_error: 59 | raise HTTPException( 60 | status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" 61 | ) 62 | return None 63 | 64 | return param 65 | 66 | 67 | class BasicAuth(SecurityBase): 68 | """Basic Auth.""" 69 | 70 | def __init__(self, scheme_name: str = None, auto_error: bool = True): 71 | self.model = SecurityBaseModel(type="http") 72 | self.scheme_name = scheme_name or self.__class__.__name__ 73 | self.auto_error = auto_error 74 | 75 | async def __call__(self, request: Request) -> Optional[str]: 76 | authorization: str = request.headers.get("Authorization") 77 | scheme, param = get_authorization_scheme_param(authorization) 78 | if not authorization or scheme.lower() != "basic": 79 | if self.auto_error: 80 | raise HTTPException( 81 | status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" 82 | ) 83 | return None 84 | return param 85 | 86 | 87 | basic_auth = BasicAuth(auto_error=False) 88 | 89 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 90 | 91 | oauth2_scheme = OAuth2PasswordBearerCookie(token_url="/token") 92 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/api/routes/authentication.py: -------------------------------------------------------------------------------- 1 | """Authentication.""" 2 | import base64 3 | from datetime import timedelta 4 | 5 | from fastapi import APIRouter, Depends, HTTPException, status 6 | from fastapi.encoders import jsonable_encoder 7 | from fastapi.openapi.docs import get_swagger_ui_html 8 | from fastapi.security import OAuth2PasswordRequestForm 9 | from starlette.responses import RedirectResponse, Response 10 | 11 | from app.api.database.models.auth import BasicAuth, basic_auth 12 | from app.api.database.models.token import Token 13 | from app.api.database.models.user import UserSchema 14 | from app.api.services import authentication_service 15 | from app.core.constant import ACCESS_TOKEN_EXPIRE_MINUTES 16 | 17 | # to get a string like this run: 18 | # openssl rand -hex 32 19 | 20 | router = APIRouter() 21 | 22 | 23 | @router.post("/token", response_model=Token) 24 | async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): 25 | """Login token.""" 26 | user = authentication_service.authenticate_user(form_data.username, form_data.password) 27 | if not user: 28 | raise HTTPException( 29 | status_code=status.HTTP_401_UNAUTHORIZED, 30 | detail="Incorrect username or password", 31 | headers={"WWW-Authenticate": "Bearer"}, 32 | ) 33 | access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) 34 | access_token = authentication_service.create_access_token( 35 | data={"sub": user.username}, expires_delta=access_token_expires 36 | ) 37 | return {"access_token": access_token, "token_type": "bearer"} 38 | 39 | 40 | @router.get("/logout") 41 | async def route_logout_and_remove_cookie(): 42 | """Logout and remove cookie.""" 43 | response = RedirectResponse(url="/") 44 | response.delete_cookie("Authorization") 45 | return response 46 | 47 | 48 | @router.get("/login") 49 | async def login_basic(auth: BasicAuth = Depends(basic_auth)): 50 | """Login and get token.""" 51 | if not auth: 52 | return Response(headers={"WWW-Authenticate": "Basic"}, status_code=401) 53 | 54 | try: 55 | decoded = base64.b64decode(auth).decode("ascii") 56 | username, _, password = decoded.partition(":") 57 | user = authentication_service.authenticate_user(username, password) 58 | if not user: 59 | raise HTTPException(status_code=400, detail="Incorrect email or password") 60 | 61 | access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) 62 | access_token = authentication_service.create_access_token( 63 | data={"sub": username}, expires_delta=access_token_expires 64 | ) 65 | 66 | token = jsonable_encoder(access_token) 67 | 68 | response = RedirectResponse(url="/docs") 69 | response.set_cookie( 70 | "Authorization", 71 | value=f"Bearer {token}", 72 | httponly=True, 73 | max_age=1800, 74 | expires=1800, 75 | ) 76 | return response 77 | 78 | except HTTPException: 79 | return Response(headers={"WWW-Authenticate": "Basic"}, status_code=401) 80 | 81 | 82 | @router.get("/docs") 83 | # pylint: disable=unused-argument 84 | async def get_documentation(current_user: UserSchema = Depends(authentication_service.get_current_active_user)): 85 | """Get documentation.""" 86 | return get_swagger_ui_html(openapi_url="/openapi.json", title="docs") 87 | 88 | 89 | @router.get("/users/me", response_model=UserSchema) 90 | async def read_users_me(current_user: UserSchema = Depends(authentication_service.get_current_active_user)): 91 | """Get information of current user.""" 92 | return current_user 93 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.gitignore: -------------------------------------------------------------------------------- 1 | temp.txt 2 | app/logger/* 3 | !app/logger/logger.py 4 | app/resources/* 5 | !app/resources/.keep 6 | .env 7 | # Created by https://www.toptal.com/developers/gitignore/api/pycharm,python,jupyternotebooks,vscode,visualstudiocode 8 | # Edit at https://www.toptal.com/developers/gitignore?templates=pycharm,python,jupyternotebooks,vscode,visualstudiocode 9 | 10 | ### JupyterNotebooks ### 11 | # gitignore template for Jupyter Notebooks 12 | # website: http://jupyter.org/ 13 | 14 | .ipynb_checkpoints 15 | */.ipynb_checkpoints/* 16 | 17 | # IPython 18 | profile_default/ 19 | ipython_config.py 20 | 21 | # Remove previous ipynb_checkpoints 22 | # git rm -r .ipynb_checkpoints/ 23 | 24 | ### PyCharm ### 25 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 26 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 27 | 28 | # User-specific stuff 29 | .idea/ 30 | 31 | # CMake 32 | cmake-build-*/ 33 | 34 | # File-based project format 35 | *.iws 36 | 37 | # IntelliJ 38 | out/ 39 | # JIRA plugin 40 | atlassian-ide-plugin.xml 41 | 42 | # Crashlytics plugin (for Android Studio and IntelliJ) 43 | com_crashlytics_export_strings.xml 44 | crashlytics.properties 45 | crashlytics-build.properties 46 | fabric.properties 47 | 48 | 49 | ### Python ### 50 | # Byte-compiled / optimized / DLL files 51 | __pycache__/ 52 | *.py[cod] 53 | *$py.class 54 | 55 | # C extensions 56 | *.so 57 | 58 | # Distribution / packaging 59 | .Python 60 | build/ 61 | develop-eggs/ 62 | dist/ 63 | downloads/ 64 | eggs/ 65 | .eggs/ 66 | parts/ 67 | sdist/ 68 | var/ 69 | wheels/ 70 | pip-wheel-metadata/ 71 | share/python-wheels/ 72 | *.egg-info/ 73 | .installed.cfg 74 | *.egg 75 | MANIFEST 76 | 77 | # PyInstaller 78 | # Usually these files are written by a python script from a template 79 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 80 | *.manifest 81 | *.spec 82 | 83 | # Installer logs 84 | pip-log.txt 85 | pip-delete-this-directory.txt 86 | 87 | # Unit test / coverage reports 88 | htmlcov/ 89 | .tox/ 90 | .nox/ 91 | .coverage 92 | .coverage.* 93 | .cache 94 | nosetests.xml 95 | coverage.xml 96 | *.cover 97 | *.py,cover 98 | .hypothesis/ 99 | .pytest_cache/ 100 | pytestdebug.log 101 | 102 | # Translations 103 | *.mo 104 | *.pot 105 | 106 | # Django stuff: 107 | *.log 108 | local_settings.py 109 | db.sqlite3 110 | db.sqlite3-journal 111 | 112 | # Flask stuff: 113 | instance/ 114 | .webassets-cache 115 | 116 | # Scrapy stuff: 117 | .scrapy 118 | 119 | # Sphinx documentation 120 | docs/_build/ 121 | doc/_build/ 122 | 123 | # PyBuilder 124 | target/ 125 | 126 | # Jupyter Notebook 127 | 128 | # IPython 129 | 130 | # pyenv 131 | .python-version 132 | 133 | # pipenv 134 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 135 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 136 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 137 | # install all needed dependencies. 138 | #Pipfile.lock 139 | 140 | 141 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 142 | __pypackages__/ 143 | 144 | # Celery stuff 145 | celerybeat-schedule 146 | celerybeat.pid 147 | 148 | # SageMath parsed files 149 | *.sage.py 150 | 151 | # Environments 152 | # .env 153 | .env_example/ 154 | .venv/ 155 | env/ 156 | venv/ 157 | ENV/ 158 | env.bak/ 159 | venv.bak/ 160 | pythonenv* 161 | 162 | # Spyder project settings 163 | .spyderproject 164 | .spyproject 165 | 166 | # Rope project settings 167 | .ropeproject 168 | 169 | # mkdocs documentation 170 | /site 171 | 172 | # mypy 173 | .mypy_cache/ 174 | .dmypy.json 175 | dmypy.json 176 | 177 | # Pyre type checker 178 | .pyre/ 179 | 180 | # pytype static type analyzer 181 | .pytype/ 182 | 183 | # operating system-related files 184 | # file properties cache/storage on macOS 185 | *.DS_Store 186 | # thumbnail cache on Windows 187 | Thumbs.db 188 | 189 | # profiling data 190 | .prof 191 | 192 | 193 | ### VisualStudioCode ### 194 | .vscode/* 195 | !.vscode/tasks.json 196 | !.vscode/launch.json 197 | *.code-workspace 198 | 199 | ### VisualStudioCode Patch ### 200 | # Ignore all local history of files 201 | .history 202 | .ionide 203 | 204 | ### vscode ### 205 | !.vscode/settings.json 206 | !.vscode/extensions.json 207 | 208 | # file 209 | *.csv 210 | *.json 211 | *.joblib 212 | *.joblib.dat 213 | *.npy 214 | *.xlsx 215 | 216 | # End of https://www.toptal.com/developers/gitignore/api/pycharm,python,jupyternotebooks,vscode,visualstudiocode 217 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app/main.py: -------------------------------------------------------------------------------- 1 | """Start Application.""" 2 | import random 3 | import string 4 | import time 5 | 6 | import hypercorn.trio 7 | import trio 8 | from fastapi import FastAPI 9 | from fastapi.exceptions import RequestValidationError 10 | from fastapi.middleware.trustedhost import TrustedHostMiddleware 11 | from fastapi.staticfiles import StaticFiles 12 | from fastapi.templating import Jinja2Templates 13 | from starlette.exceptions import HTTPException 14 | from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint 15 | from starlette.middleware.cors import CORSMiddleware 16 | from starlette.requests import Request 17 | from starlette.responses import HTMLResponse, Response 18 | 19 | from app.api.database.migrate.init_super_user import init_super_user 20 | from app.api.errors.http_error import http_error_handler 21 | from app.api.errors.validation_error import http422_error_handler 22 | from app.api.routes import authentication 23 | from app.api.routes.api import app as api_router 24 | from app.core.config import ALLOWED_HOSTS, API_PREFIX, DEBUG, PROJECT_NAME, VERSION 25 | from app.core.constant import APP_HOST, APP_PORT 26 | from app.logger.logger import custom_logger 27 | 28 | 29 | class LoggingMiddleware(BaseHTTPMiddleware): 30 | """Logging All API request.""" 31 | 32 | async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: 33 | """Dispatch.""" 34 | idem = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) 35 | custom_logger.info(f"rid={idem} start request {request.method} {request.url.path}") 36 | start_time = time.time() 37 | 38 | # Log the request 39 | custom_logger.info("Received request: %s %s", request.method, request.url) 40 | custom_logger.debug("Request headers: %s", request.headers) 41 | custom_logger.debug("Request body: %s", await request.body()) 42 | 43 | # Call the next middleware or route handler 44 | response = await call_next(request) 45 | 46 | process_time = (time.time() - start_time) * 1000 47 | formatted_process_time = '{0:.2f}'.format(process_time) 48 | 49 | custom_logger.info( 50 | "rid=%s method=%s path=%s completed_in=%sms status_code=%s", 51 | idem, request.method, request.url.path, formatted_process_time, response.status_code, 52 | ) 53 | custom_logger.info("Response status code: %s", response.status_code) 54 | custom_logger.debug("Response headers: %s", response.headers) 55 | 56 | return response 57 | 58 | 59 | def get_application() -> FastAPI: 60 | """Get application.""" 61 | init_super_user() 62 | 63 | application = FastAPI(title=PROJECT_NAME, debug=DEBUG, version=VERSION, docs_url=None) 64 | application.add_middleware( 65 | CORSMiddleware, 66 | allow_origins=ALLOWED_HOSTS or ["*"], 67 | allow_credentials=True, 68 | allow_methods=["*"], 69 | allow_headers=["*"], 70 | ) 71 | application.add_middleware(LoggingMiddleware) 72 | # application.add_middleware(GZipMiddleware, minimum_size=1000) 73 | application.add_middleware(TrustedHostMiddleware, allowed_hosts=["*"]) 74 | 75 | application.add_exception_handler(HTTPException, http_error_handler) 76 | application.add_exception_handler(RequestValidationError, http422_error_handler) 77 | 78 | application.include_router(api_router, prefix=API_PREFIX) 79 | application.include_router(authentication.router, tags=["Authentication"]) 80 | 81 | application.mount("/static", StaticFiles(directory="app/frontend/static"), name="static") 82 | 83 | templates = Jinja2Templates(directory="app/frontend/templates") 84 | 85 | @application.get("/", tags=["UI"], response_class=HTMLResponse, deprecated=False) 86 | async def read_root(request: Request): 87 | return templates.TemplateResponse("index.html", {"request": request}) 88 | 89 | @application.get("/logger", response_class=HTMLResponse, deprecated=False) 90 | async def get_logger(): 91 | with open("app/logger/logger.log", "r") as f: 92 | log_str = f.read() 93 | log_html = f"
{log_str}
" 94 | return log_html 95 | 96 | return application 97 | 98 | 99 | async def app_handler(scope, receive, send): 100 | await app(scope, receive, send) 101 | 102 | 103 | async def main(): 104 | config = hypercorn.trio.Config.from_mapping( 105 | bind=[f"{APP_HOST}:{APP_PORT}"], 106 | workers=1, 107 | ) 108 | await hypercorn.trio.serve(app_handler, config) 109 | 110 | 111 | app = get_application() 112 | 113 | if __name__ == "__main__": 114 | trio.run(main) 115 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | extension-pkg-whitelist=pydantic 3 | 4 | # Specify a configuration file. 5 | #rcfile= 6 | 7 | # Python code to execute, usually for sys.path manipulation such as 8 | # pygtk.require(). 9 | #init-hook= 10 | 11 | # Files or directories to be skipped. They should be base names, not 12 | # paths. 13 | ignore=CVS 14 | 15 | # Add files or directories matching the regex patterns to the ignore-list. The 16 | # regex matches against paths and can be in Posix or Windows format. 17 | ignore-paths= 18 | 19 | # Files or directories matching the regex patterns are skipped. The regex 20 | # matches against base names, not paths. 21 | ignore-patterns=^\.# 22 | 23 | # Pickle collected data for later comparisons. 24 | persistent=yes 25 | 26 | # List of plugins (as comma separated values of python modules names) to load, 27 | # usually to register additional checkers. 28 | load-plugins= 29 | pylint.extensions.check_elif, 30 | pylint.extensions.bad_builtin, 31 | pylint.extensions.docparams, 32 | pylint.extensions.for_any_all, 33 | pylint.extensions.set_membership, 34 | pylint.extensions.code_style, 35 | pylint.extensions.overlapping_exceptions, 36 | pylint.extensions.typing, 37 | pylint.extensions.redefined_variable_type, 38 | pylint.extensions.comparison_placement, 39 | 40 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 41 | # number of processors available to use. 42 | jobs=1 43 | 44 | # When enabled, pylint would attempt to guess common misconfiguration and emit 45 | # user-friendly hints instead of false-positive error messages. 46 | suggestion-mode=yes 47 | 48 | # Allow loading of arbitrary C extensions. Extensions are imported into the 49 | # active Python interpreter and may run arbitrary code. 50 | unsafe-load-any-extension=no 51 | 52 | # A comma-separated list of package or module names from where C extensions may 53 | # be loaded. Extensions are loading into the active Python interpreter and may 54 | # run arbitrary code 55 | extension-pkg-allow-list= 56 | 57 | # Minimum supported python version 58 | py-version = 3.7.2 59 | 60 | # Control the amount of potential inferred values when inferring a single 61 | # object. This can help the performance when dealing with large functions or 62 | # complex, nested conditions. 63 | limit-inference-results=100 64 | 65 | # Specify a score threshold to be exceeded before program exits with error. 66 | fail-under=10.0 67 | 68 | # Return non-zero exit code if any of these messages/categories are detected, 69 | # even if score is above --fail-under value. Syntax same as enable. Messages 70 | # specified are enabled, while categories only check already-enabled messages. 71 | fail-on= 72 | 73 | 74 | [MESSAGES CONTROL] 75 | 76 | # Only show warnings with the listed confidence levels. Leave empty to show 77 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 78 | # confidence= 79 | 80 | # Enable the message, report, category or checker with the given id(s). You can 81 | # either give multiple identifier separated by comma (,) or put this option 82 | # multiple time (only on the command line, not in the configuration file where 83 | # it should appear only once). See also the "--disable" option for examples. 84 | enable= 85 | use-symbolic-message-instead, 86 | useless-suppression, 87 | 88 | # Disable the message, report, category or checker with the given id(s). You 89 | # can either give multiple identifiers separated by comma (,) or put this 90 | # option multiple times (only on the command line, not in the configuration 91 | # file where it should appear only once).You can also use "--disable=all" to 92 | # disable everything first and then re-enable specific checks. For example, if 93 | # you want to run only the similarities checker, you can use "--disable=all 94 | # --enable=similarities". If you want to run only the classes checker, but have 95 | # no Warning level messages displayed, use"--disable=all --enable=classes 96 | # --disable=W" 97 | 98 | disable= 99 | attribute-defined-outside-init, 100 | invalid-name, 101 | bad-builtin, 102 | protected-access, 103 | too-few-public-methods, 104 | # handled by black 105 | format, 106 | # We anticipate #3512 where it will become optional 107 | fixme, 108 | cyclic-import, 109 | no-name-in-module, 110 | no-self-argument, 111 | no-member, 112 | c-extension-no-member, 113 | unbalanced-tuple-unpacking, 114 | 115 | 116 | [REPORTS] 117 | 118 | # Set the output format. Available formats are text, parseable, colorized, msvs 119 | # (visual studio) and html. You can also give a reporter class, eg 120 | # mypackage.mymodule.MyReporterClass. 121 | output-format=text 122 | 123 | # Tells whether to display a full report or only the messages 124 | reports=no 125 | 126 | # Python expression which should return a note less than 10 (10 is the highest 127 | # note). You have access to the variables 'fatal', 'error', 'warning', 'refactor', 'convention' 128 | # and 'info', which contain the number of messages in each category, as 129 | # well as 'statement', which is the total number of statements analyzed. This 130 | # score is used by the global evaluation report (RP0004). 131 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 132 | 133 | # Template used to display messages. This is a python new-style format string 134 | # used to format the message information. See doc for all details 135 | #msg-template= 136 | 137 | # Activate the evaluation score. 138 | score=yes 139 | 140 | 141 | [LOGGING] 142 | 143 | # Logging modules to check that the string format arguments are in logging 144 | # function parameter format 145 | logging-modules=logging 146 | 147 | # The type of string formatting that logging methods do. `old` means using % 148 | # formatting, `new` is for `{}` formatting. 149 | logging-format-style=old 150 | 151 | 152 | [MISCELLANEOUS] 153 | 154 | # List of note tags to take in consideration, separated by a comma. 155 | notes=FIXME,XXX,TODO 156 | 157 | # Regular expression of note tags to take in consideration. 158 | #notes-rgx= 159 | 160 | 161 | [SIMILARITIES] 162 | 163 | # Minimum lines number of a similarity. 164 | min-similarity-lines=6 165 | 166 | # Ignore comments when computing similarities. 167 | ignore-comments=yes 168 | 169 | # Ignore docstrings when computing similarities. 170 | ignore-docstrings=yes 171 | 172 | # Ignore imports when computing similarities. 173 | ignore-imports=yes 174 | 175 | # Signatures are removed from the similarity computation 176 | ignore-signatures=yes 177 | 178 | 179 | [VARIABLES] 180 | 181 | # Tells whether we should check for unused import in __init__ files. 182 | init-import=no 183 | 184 | # A regular expression matching the name of dummy variables (i.e. expectedly 185 | # not used). 186 | dummy-variables-rgx=_$|dummy 187 | 188 | # List of additional names supposed to be defined in builtins. Remember that 189 | # you should avoid defining new builtins when possible. 190 | additional-builtins= 191 | 192 | # List of strings which can identify a callback function by name. A callback 193 | # name must start or end with one of those strings. 194 | callbacks=cb_,_cb 195 | 196 | # Tells whether unused global variables should be treated as a violation. 197 | allow-global-unused-variables=yes 198 | 199 | # List of names allowed to shadow builtins 200 | allowed-redefined-builtins= 201 | 202 | # Argument names that match this expression will be ignored. Default to name 203 | # with leading underscore. 204 | ignored-argument-names=_.* 205 | 206 | # List of qualified module names which can have objects that can redefine 207 | # builtins. 208 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 209 | 210 | 211 | [FORMAT] 212 | 213 | # Maximum number of characters on a single line. 214 | max-line-length=120 215 | 216 | # Regexp for a line that is allowed to be longer than the limit. 217 | ignore-long-lines=^\s*(# )??$ 218 | 219 | # Allow the body of an if to be on the same line as the test if there is no 220 | # else. 221 | single-line-if-stmt=no 222 | 223 | # Allow the body of a class to be on the same line as the declaration if body 224 | # contains single statement. 225 | single-line-class-stmt=no 226 | 227 | # Maximum number of lines in a module 228 | max-module-lines=2000 229 | 230 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 231 | # tab). 232 | indent-string=' ' 233 | 234 | # Number of spaces of indent required inside a hanging or continued line. 235 | indent-after-paren=4 236 | 237 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 238 | expected-line-ending-format= 239 | 240 | 241 | [BASIC] 242 | 243 | # Good variable names which should always be accepted, separated by a comma 244 | good-names=i,j,k,ex,Run,_ 245 | 246 | # Good variable names regexes, separated by a comma. If names match any regex, 247 | # they will always be accepted 248 | good-names-rgxs= 249 | 250 | # Bad variable names which should always be refused, separated by a comma 251 | bad-names=foo,bar,baz,toto,tutu,tata 252 | 253 | # Bad variable names regexes, separated by a comma. If names match any regex, 254 | # they will always be refused 255 | bad-names-rgxs= 256 | 257 | # Colon-delimited sets of names that determine each other's naming style when 258 | # the name regexes allow several styles. 259 | name-group= 260 | 261 | # Include a hint for the correct naming format with invalid-name 262 | include-naming-hint=no 263 | 264 | # Naming style matching correct function names. 265 | function-naming-style=snake_case 266 | 267 | # Regular expression matching correct function names 268 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 269 | 270 | # Naming style matching correct variable names. 271 | variable-naming-style=snake_case 272 | 273 | # Regular expression matching correct variable names 274 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 275 | 276 | # Naming style matching correct constant names. 277 | const-naming-style=UPPER_CASE 278 | 279 | # Regular expression matching correct constant names 280 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 281 | 282 | # Naming style matching correct attribute names. 283 | attr-naming-style=snake_case 284 | 285 | # Regular expression matching correct attribute names 286 | attr-rgx=[a-z_][a-z0-9_]{2,}$ 287 | 288 | # Naming style matching correct argument names. 289 | argument-naming-style=snake_case 290 | 291 | # Regular expression matching correct argument names 292 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 293 | 294 | # Naming style matching correct class attribute names. 295 | class-attribute-naming-style=any 296 | 297 | # Regular expression matching correct class attribute names 298 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 299 | 300 | # Naming style matching correct class constant names. 301 | class-const-naming-style=UPPER_CASE 302 | 303 | # Regular expression matching correct class constant names. Overrides class- 304 | # const-naming-style. 305 | #class-const-rgx= 306 | 307 | # Naming style matching correct inline iteration names. 308 | inlinevar-naming-style=any 309 | 310 | # Regular expression matching correct inline iteration names 311 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 312 | 313 | # Naming style matching correct class names. 314 | class-naming-style=PascalCase 315 | 316 | # Regular expression matching correct class names 317 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 318 | 319 | 320 | # Naming style matching correct module names. 321 | module-naming-style=snake_case 322 | 323 | # Regular expression matching correct module names 324 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 325 | 326 | 327 | # Naming style matching correct method names. 328 | method-naming-style=snake_case 329 | 330 | # Regular expression matching correct method names 331 | method-rgx=[a-z_][a-z0-9_]{2,}$ 332 | 333 | # Regular expression which can overwrite the naming style set by typevar-naming-style. 334 | #typevar-rgx= 335 | 336 | # Regular expression which should only match function or class names that do 337 | # not require a docstring. Use ^(?!__init__$)_ to also check __init__. 338 | no-docstring-rgx=__.*__ 339 | 340 | # Minimum line length for functions/classes that require docstrings, shorter 341 | # ones are exempt. 342 | docstring-min-length=-1 343 | 344 | # List of decorators that define properties, such as abc.abstractproperty. 345 | property-classes=abc.abstractproperty 346 | 347 | 348 | [TYPECHECK] 349 | 350 | # Regex pattern to define which classes are considered mixins if ignore-mixin- 351 | # members is set to 'yes' 352 | mixin-class-rgx=.*MixIn 353 | 354 | # List of module names for which member attributes should not be checked 355 | # (useful for modules/projects where namespaces are manipulated during runtime 356 | # and thus existing member attributes cannot be deduced by static analysis). It 357 | # supports qualified module names, as well as Unix pattern matching. 358 | ignored-modules= 359 | 360 | # List of class names for which member attributes should not be checked (useful 361 | # for classes with dynamically set attributes). This supports the use of 362 | # qualified names. 363 | ignored-classes=SQLObject, optparse.Values, thread._local, _thread._local 364 | 365 | # List of members which are set dynamically and missed by pylint inference 366 | # system, and so shouldn't trigger E1101 when accessed. Python regular 367 | # expressions are accepted. 368 | generated-members=REQUEST,acl_users,aq_parent,argparse.Namespace 369 | 370 | # List of decorators that create context managers from functions, such as 371 | # contextlib.contextmanager. 372 | contextmanager-decorators=contextlib.contextmanager 373 | 374 | # Tells whether to warn about missing members when the owner of the attribute 375 | # is inferred to be None. 376 | ignore-none=yes 377 | 378 | # This flag controls whether pylint should warn about no-member and similar 379 | # checks whenever an opaque object is returned when inferring. The inference 380 | # can return multiple potential results while evaluating a Python object, but 381 | # some branches might not be evaluated, which results in partial inference. In 382 | # that case, it might be useful to still emit no-member and other checks for 383 | # the rest of the inferred objects. 384 | ignore-on-opaque-inference=yes 385 | 386 | # Show a hint with possible names when a member name was not found. The aspect 387 | # of finding the hint is based on edit distance. 388 | missing-member-hint=yes 389 | 390 | # The minimum edit distance a name should have in order to be considered a 391 | # similar match for a missing member name. 392 | missing-member-hint-distance=1 393 | 394 | # The total number of similar names that should be taken in consideration when 395 | # showing a hint for a missing member. 396 | missing-member-max-choices=1 397 | 398 | [SPELLING] 399 | 400 | # Spelling dictionary name. Available dictionaries: none. To make it working 401 | # install python-enchant package. 402 | spelling-dict= 403 | 404 | # List of comma separated words that should not be checked. 405 | spelling-ignore-words= 406 | 407 | # List of comma separated words that should be considered directives if they 408 | # appear and the beginning of a comment and should not be checked. 409 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:,pragma:,# noinspection 410 | 411 | # A path to a file that contains private dictionary; one word per line. 412 | spelling-private-dict-file=.pyenchant_pylint_custom_dict.txt 413 | 414 | # Tells whether to store unknown words to indicated private dictionary in 415 | # --spelling-private-dict-file option instead of raising a message. 416 | spelling-store-unknown-words=no 417 | 418 | # Limits count of emitted suggestions for spelling mistakes. 419 | max-spelling-suggestions=2 420 | 421 | 422 | [DESIGN] 423 | 424 | # Maximum number of arguments for function / method 425 | max-args=10 426 | 427 | # Maximum number of locals for function / method body 428 | max-locals=25 429 | 430 | # Maximum number of return / yield for function / method body 431 | max-returns=11 432 | 433 | # Maximum number of branch for function / method body 434 | max-branches=27 435 | 436 | # Maximum number of statements in function / method body 437 | max-statements=100 438 | 439 | # Maximum number of parents for a class (see R0901). 440 | max-parents=7 441 | 442 | # List of qualified class names to ignore when counting class parents (see R0901). 443 | ignored-parents= 444 | 445 | # Maximum number of attributes for a class (see R0902). 446 | max-attributes=11 447 | 448 | # Minimum number of public methods for a class (see R0903). 449 | min-public-methods=2 450 | 451 | # Maximum number of public methods for a class (see R0904). 452 | max-public-methods=25 453 | 454 | # Maximum number of boolean expressions in an if statement (see R0916). 455 | max-bool-expr=5 456 | 457 | # List of regular expressions of class ancestor names to 458 | # ignore when counting public methods (see R0903). 459 | exclude-too-few-public-methods= 460 | 461 | [CLASSES] 462 | 463 | # List of method names used to declare (i.e. assign) instance attributes. 464 | defining-attr-methods=__init__,__new__,setUp,__post_init__ 465 | 466 | # List of valid names for the first argument in a class method. 467 | valid-classmethod-first-arg=cls 468 | 469 | # List of valid names for the first argument in a metaclass class method. 470 | valid-metaclass-classmethod-first-arg=mcs 471 | 472 | # List of member names, which should be excluded from the protected access 473 | # warning. 474 | exclude-protected=_asdict,_fields,_replace,_source,_make 475 | 476 | # Warn about protected attribute access inside special methods 477 | check-protected-access-in-special-methods=no 478 | 479 | [IMPORTS] 480 | 481 | # List of modules that can be imported at any level, not just the top level 482 | # one. 483 | allow-any-import-level= 484 | 485 | # Allow wildcard imports from modules that define __all__. 486 | allow-wildcard-with-all=no 487 | 488 | # Analyse import fallback blocks. This can be used to support both Python 2 and 489 | # 3 compatible code, which means that the block might have code that exists 490 | # only in one or another interpreter, leading to false positives when analysed. 491 | analyse-fallback-blocks=no 492 | 493 | # Deprecated modules which should not be used, separated by a comma 494 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 495 | 496 | # Create a graph of every (i.e. internal and external) dependencies in the 497 | # given file (report RP0402 must not be disabled) 498 | import-graph= 499 | 500 | # Create a graph of external dependencies in the given file (report RP0402 must 501 | # not be disabled) 502 | ext-import-graph= 503 | 504 | # Create a graph of internal dependencies in the given file (report RP0402 must 505 | # not be disabled) 506 | int-import-graph= 507 | 508 | # Force import order to recognize a module as part of the standard 509 | # compatibility libraries. 510 | known-standard-library= 511 | 512 | # Force import order to recognize a module as part of a third party library. 513 | known-third-party=enchant 514 | 515 | # Couples of modules and preferred modules, separated by a comma. 516 | preferred-modules= 517 | 518 | 519 | [EXCEPTIONS] 520 | 521 | # Exceptions that will emit a warning when being caught. Defaults to 522 | # "Exception" 523 | overgeneral-exceptions=Exception 524 | 525 | 526 | [TYPING] 527 | 528 | # Set to ``no`` if the app / library does **NOT** need to support runtime 529 | # introspection of type annotations. If you use type annotations 530 | # **exclusively** for type checking of an application, you're probably fine. 531 | # For libraries, evaluate if some users what to access the type hints at 532 | # runtime first, e.g., through ``typing.get_type_hints``. Applies to Python 533 | # versions 3.7 - 3.9 534 | runtime-typing = no 535 | 536 | 537 | [DEPRECATED_BUILTINS] 538 | 539 | # List of builtins function names that should not be used, separated by a comma 540 | bad-functions=map,input 541 | 542 | 543 | [REFACTORING] 544 | 545 | # Maximum number of nested blocks for function / method body 546 | max-nested-blocks=5 547 | 548 | # Complete name of functions that never returns. When checking for 549 | # inconsistent-return-statements if a never returning function is called then 550 | # it will be considered as an explicit return statement and no message will be 551 | # printed. 552 | never-returning-functions=sys.exit,argparse.parse_error 553 | 554 | 555 | [STRING] 556 | 557 | # This flag controls whether inconsistent-quotes generates a warning when the 558 | # character used as a quote delimiter is used inconsistently within a module. 559 | check-quote-consistency=no 560 | 561 | # This flag controls whether the implicit-str-concat should generate a warning 562 | # on implicit string concatenation in sequences defined over several lines. 563 | check-str-concat-over-line-jumps=no 564 | 565 | 566 | [CODE_STYLE] 567 | 568 | # Max line length for which to sill emit suggestions. Used to prevent optional 569 | # suggestions which would get split by a code formatter (e.g., black). Will 570 | # default to the setting for ``max-line-length``. 571 | #max-line-length-suggestions= 572 | --------------------------------------------------------------------------------