FAST API project scaffolder
import pathlib as pl
import os

PACKAGE_LOCATION = pl.Path(os.path.dirname(__file__))
RESOURCE_LOCATION = PACKAGE_LOCATION / "resources"
project_templates/fastapi/{{cookiecutter.name}}/tests/__init__.py
giuseppeambrosio97/tigr81
from fastapi import status
from fastapi.testclient import TestClient

from {{cookiecutter.package_name}}.app import app

client = TestClient(app)

def test_router_health():
    with TestClient(app) as client:
        response = client.get("/health")
        assert response.status_code == status.HTTP_200_OK
project_templates/fastapi/{{cookiecutter.name}}/tests/routers/test_health.py
giuseppeambrosio97/tigr81
import os
import pathlib as pl

__version__ = "1.0.0"

PACKAGE_LOCATION = pl.Path(os.path.dirname(__file__))
ROOT_LOCATION = PACKAGE_LOCATION.parent
RESOURCES_LOCATION = PACKAGE_LOCATION / "resources"
CONFIG_LOCATION = RESOURCES_LOCATION / "config"
LOGGING_CONF_LOCATION = CONFIG_LOCATION / "logging.conf"

project_templates/fastapi/{{cookiecutter.name}}/{{cookiecutter.package_name}}/__init__.py
giuseppeambrosio97/tigr81
import logging

import uvicorn
from fastapi import Depends, FastAPI

from {{cookiecutter.package_name}} import LOGGING_CONF_LOCATION, dependencies
from {{cookiecutter.package_name}}.config.model import API_CONFIG
from {{cookiecutter.package_name}}.routers import health


logger = logging.getLogger(__name__)

app = FastAPI(
    dependencies=[Depends(dependencies.get_apikey_header)],
)

app.include_router(health.router)

if __name__ == "__main__":
    uvicorn.run("app:app", 
        host=API_CONFIG.host, 
        port=API_CONFIG.port, 
        reload=API_CONFIG.reload,
        log_config=LOGGING_CONF_LOCATION.as_posix()
    )
project_templates/fastapi/{{cookiecutter.name}}/{{cookiecutter.package_name}}/app.py
giuseppeambrosio97/tigr81
import os
from argparse import ArgumentParser

import yaml
from {{cookiecutter.package_name}} import CONFIG_LOCATION


def _merge_dictionaries(main_dict, new_dict, merging_lists=False):
    """ Merge two dictionaries prioritizing the second one """

    # Copy the dict
    main_dict = main_dict.copy()

    # Get all the keys
    main_keys = main_dict.keys()
    new_keys = new_dict.keys()
    keys = set(main_keys).union(new_keys)

    # For each key, merge the dict
    for key in keys:
        # If key is just in new, add it to main
        if key not in main_keys:
            main_dict[key] = new_dict[key]
        # If key is in both dicts, and...
        elif key in new_keys:
            # ...the values are dicts, merge them recursively
            if isinstance(main_dict[key], dict) and isinstance(new_dict[key], dict):
                main_dict[key] = _merge_dictionaries(main_dict[key], new_dict[key], merging_lists=merging_lists)
            # ...the value is NOT a dict in both cases, override the main value
            else:
                # If merging lists is True, and values are lists, concatenate the elements
                if merging_lists and isinstance(main_dict[key], list) and isinstance(new_dict[key], list):
                    main_dict[key] = main_dict[key] + new_dict[key]
                # Override them otherwise
                else:
                    main_dict[key] = new_dict[key]
        # If key is just in main, nothing to do: just keep main_dict as it is

    return main_dict


def _get_config_dict(env):
    """ Get the config dictionary from resource file """

    with open(os.path.join(CONFIG_LOCATION, "{}.yml".format(env))) as f:
        configmap = yaml.load(f, Loader=yaml.SafeLoader)
    return configmap if configmap else {}


def _get_env():
    """ Get the environment (first from command line, otherwise from env variable, otherwise local) """

    parser = ArgumentParser()
    parser.add_argument("-e", "--environment",
                        default=os.environ.get("ENVIRONMENT", "local"),
                        help='environment')
    args, _ = parser.parse_known_args()
    return args.environment


environment = _get_env()
config = _merge_dictionaries(_get_config_dict("default"), _get_config_dict(environment))

project_templates/fastapi/{{cookiecutter.name}}/{{cookiecutter.package_name}}/config/loading.py
giuseppeambrosio97/tigr81
from dataclasses import dataclass

from {{cookiecutter.package_name}}.config.loading import config

@dataclass(frozen=True)
class ApiConfig:
    host: str
    port: int
    reload: bool


API_CONFIG = ApiConfig(**config["api_config"])
project_templates/fastapi/{{cookiecutter.name}}/{{cookiecutter.package_name}}/config/model.py
giuseppeambrosio97/tigr81
import logging
import os
import secrets

from fastapi import HTTPException, Security, status
from fastapi.security import APIKeyHeader

logger = logging.getLogger(__name__)

api_key_header = APIKeyHeader(name="X-API-Key")

async def get_apikey_header(apikey: str = Security(api_key_header)):
    """
        This function checks whether a request has the correct apikey.

    Args:
        apikey (str, optional): apikey taken from the header of the http request. Defaults to Header(...).

    Raises:
        HTTPException: The function raise an exception if the apikey is incorrect.
    """
    logger.info("apikey authentication in progress")
    if not secrets.compare_digest(apikey, os.getenv("API_KEY")):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED, 
            detail="Invalid or missing API Key"
        )

project_templates/fastapi/{{cookiecutter.name}}/{{cookiecutter.package_name}}/dependencies.py
giuseppeambrosio97/tigr81
from fastapi import APIRouter, status

router = APIRouter()

@router.get("/health", status_code=status.HTTP_200_OK)
async def health_check():
    return {"health_check": "OK"}

project_templates/fastapi/{{cookiecutter.name}}/{{cookiecutter.package_name}}/routers/health.py
giuseppeambrosio97/tigr81
import pathlib as pl
import os

__version__ = "1.0.0"

PACKAGE_LOCATION = pl.Path(os.path.dirname(__file__))
ROOT_LOCATION = PACKAGE_LOCATION.parent
REPO_LOCATION = ROOT_LOCATION.parent

LOCAL_PROJECT_TEMPLATES_FOLDER_LOCATION = REPO_LOCATION / "project_templates"
RESOURCE_LOCATION = PACKAGE_LOCATION / "resources"

tigr81/tigr81/__init__.py
giuseppeambrosio97/tigr81
from enum import Enum

from pydantic import BaseModel, EmailStr


class ProjectTypeEnum(str, Enum):
    FAST_API = "fastapi"
    # GCP_CLOUD_FUNCTION = "gcp_cloud_function"
    # COOKIECUTTER = "cookiecutter"
    # POETRY = "poetry"
    # MONO_REPO = "mono_repo"

    def __str__(self):
        return self.value

class Author(BaseModel):
    name: str
    email: EmailStr


class ProjectTemplateOptions(BaseModel):
    name: str
    package_name: str
    description: str
    author: Author


class ProjectTemplate(BaseModel):
    type: ProjectTypeEnum
    project_options: ProjectTemplateOptions

    class Config:
        use_enum_values = True
tigr81/tigr81/commands/models.py
giuseppeambrosio97/tigr81
import os

from cookiecutter.main import cookiecutter
import typer

from tigr81 import REPO_LOCATION
from tigr81.commands.models import (
    Author,
    ProjectTemplate,
    ProjectTemplateOptions,
    ProjectTypeEnum
)

REPO_TEMPLATES = "https://github.com/giuseppeambrosio97/tigr81.git"


app = typer.Typer()


@app.callback()
def callback():
    """
    Scaffold project templates
    """


@app.command()
def scaffold(
    project_type: ProjectTypeEnum,
    default: bool = typer.Option(
        False, help="Set to False to enable input during cookiecutter execution"
    ),
):
    """Scaffold a project template"""
    PROJECT_TEMPLATE_LOCATION = REPO_TEMPLATES
    checkout = "develop"

    if os.getenv("TIGR81_ENVIRONMENT") == "local":
        PROJECT_TEMPLATE_LOCATION = REPO_LOCATION.as_posix()
        checkout = None

    typer.echo(
        f"Scaffolding a {project_type} project template from {PROJECT_TEMPLATE_LOCATION}"
    )

    project_template = ProjectTemplate(
        type=project_type,
        project_options=ProjectTemplateOptions(
            name="fastapi-app",
            package_name="fastapi_pkg",
            description="FAST API web app",
            author=Author(name="author name", email="authornemail@gmail.com")
        )
    )

    cookiecutter(
        template=PROJECT_TEMPLATE_LOCATION,
        output_dir=".",
        no_input=default,
        extra_context=project_template.project_options.model_dump(),
        checkout=checkout,
        directory=f"project_templates/{project_template.type}",
    )

tigr81/tigr81/commands/scaffold.py
giuseppeambrosio97/tigr81
import typer
from tigr81.commands import scaffold

app = typer.Typer()

app.add_typer(scaffold.app(), name="scaffold")

tigr81/tigr81/main.py
giuseppeambrosio97/tigr81