├── {{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 | 
3 | 
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 |
--------------------------------------------------------------------------------