use std::{
fs::{create_dir_all, File},
path::Path,
};
use anyhow::Result;
use rayon::prelude::*;
use crate::{
fastapi::{
core_files::{
save_cache_file, save_config_file, save_core_utils_file, save_db_file,
save_security_file,
},
docker_files::{
save_dockercompose_file, save_dockercompose_override_file,
save_dockercompose_traefik_file, save_dockerfile, save_dockerfileignore,
save_entrypoint_script,
},
migration_files::save_initial_migrations,
model_files::{save_message_model_file, save_token_models_file, save_user_models_file},
route_files::{
save_deps_file, save_health_route, save_login_route, save_router_file,
save_users_route, save_version_route,
},
service_files::{save_cache_user_services_file, save_db_user_services_file},
test_files::{
save_config_test_file, save_conftest_file, save_health_route_test_file,
save_login_route_test_file, save_main_test_file, save_test_deps_file,
save_test_utils_file, save_user_model_test_file, save_user_routes_test_file,
save_user_services_cache_test_file, save_user_services_db_test_file,
save_version_route_test_file,
},
},
file_manager::save_file_with_content,
project_info::{DatabaseManager, ProjectInfo},
utils::is_python_version_or_greater,
};
pub fn generate_fastapi(project_info: &ProjectInfo) -> Result<()> {
create_directories(project_info)?;
[
save_cache_file,
save_cache_user_services_file,
save_config_test_file,
save_conftest_file,
save_db_file,
save_db_user_services_file,
save_dockercompose_file,
save_dockercompose_override_file,
save_dockercompose_traefik_file,
save_dockerfileignore,
save_dockerfile,
save_entrypoint_script,
save_example_env_file,
save_exceptions_file,
save_initial_migrations,
save_main_file,
save_main_test_file,
save_message_model_file,
save_config_file,
save_core_utils_file,
save_deps_file,
save_health_route,
save_health_route_test_file,
save_login_route,
save_login_route_test_file,
save_router_file,
save_security_file,
save_test_deps_file,
save_test_utils_file,
save_token_models_file,
save_types_file,
save_user_models_file,
save_user_model_test_file,
save_users_route,
save_user_routes_test_file,
save_user_services_cache_test_file,
save_user_services_db_test_file,
save_version_route,
save_version_route_test_file,
]
.into_par_iter()
.map(|f| f(project_info))
.collect::<Result<Vec<_>, _>>()?;
Ok(())
}
fn create_example_env_file(project_info: &ProjectInfo) -> String {
let mut info = r#"SECRET_KEY=someKey
FIRST_SUPERUSER_EMAIL=some@email.com
FIRST_SUPERUSER_PASSWORD=changethis
FIRST_SUPERUSER_NAME="Wade Watts"
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=some_password
POSTGRES_DB=changethis
VALKEY_HOST=127.0.0.1
VALKEY_PASSWORD=test_password
STACK_NAME=changethis
DOMAIN=127.0.0.1
PRODUCTION_MODE=false
"#
.to_string();
if let Some(database_manager) = &project_info.database_manager {
if database_manager == &DatabaseManager::AsyncPg {
info.push_str("DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:{POSTGRES_PORT}/${POSTGRES_DB}\n");
}
}
info
}
fn save_example_env_file(project_info: &ProjectInfo) -> Result<()> {
let base = &project_info.base_dir();
let file_path = base.join(".env-example");
let file_content = create_example_env_file(project_info);
save_file_with_content(&file_path, &file_content)?;
Ok(())
}
fn create_exceptions_file() -> String {
r#"class DbInsertError(Exception):
pass
class DbUpdateError(Exception):
pass
class NoDbPoolError(Exception):
pass
class UserNotFoundError(Exception):
pass
"#
.to_string()
}
fn save_exceptions_file(project_info: &ProjectInfo) -> Result<()> {
let base = &project_info.source_dir_path();
let file_path = base.join("exceptions.py");
let file_content = create_exceptions_file();
save_file_with_content(&file_path, &file_content)?;
Ok(())
}
fn create_main_file(project_info: &ProjectInfo) -> String {
let module = &project_info.module_name();
format!(
r#"from __future__ import annotations
import sys
import traceback
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, Response
from loguru import logger
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import JSONResponse
from {module}.api.router import api_router
from {module}.core.cache import cache
from {module}.core.config import settings
from {module}.core.db import db
logger.remove() # Remove the default logger so log level can be set
logger.add(sys.stderr, level=settings.LOG_LEVEL)
@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncGenerator: # pragma: no cover
logger.info("Initializing database connection pool")
try:
await db.create_pool()
except Exception as e:
logger.error(f"Error creating db connection pool: {{e}}")
raise
logger.info("Initializing cache client")
try:
await cache.create_client()
except Exception as e:
logger.error(f"Error creating cache client: {{e}}")
raise
logger.info("Saving first superuser")
try:
await db.create_first_superuser()
except Exception as e:
logger.error(f"Error creating first superuser: {{e}}")
raise e
yield
logger.info("Closing database connection pool")
try:
await db.close_pool()
except Exception as e:
logger.error(f"Error closing db connection pool: {{e}}")
raise
logger.info("Closing cache client")
try:
await cache.close_client()
except Exception as e:
logger.error(f"Error closing cache client: {{e}}")
raise
openapi_url = f"{{settings.API_V1_PREFIX}}/openapi.json"
app = FastAPI(
title=settings.TITLE,
lifespan=lifespan,
openapi_url=openapi_url,
)
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> Response:
if exc.status_code >= 500:
stack_trace = (
"".join(
traceback.format_exception(
type(exc.__cause__), exc.__cause__, exc.__cause__.__traceback__
)
)
if exc.__cause__
else traceback.format_exc()
)
original_exc_type = type(exc.__cause__).__name__ if exc.__cause__ else "HTTPException"
original_exc_msg = str(exc.__cause__) if exc.__cause__ else str(exc.detail)
msg = f"""HTTP {{exc.status_code}} error in {{request.method}} {{request.url.path}}\n
Original exception: {{original_exc_type}}: {{original_exc_msg}}\n
HTTP detail: {{exc.detail}}\n
Stack trace:\n{{stack_trace}}"""
logger.error(msg)
return JSONResponse(status_code=exc.status_code, content={{"detail": exc.detail}})
if settings.all_cors_origins:
app.add_middleware(
CORSMiddleware,
allow_origins=settings.all_cors_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allow_headers=["Authorization", "Content-Type"],
)
app.include_router(api_router)
"#
)
}
fn save_main_file(project_info: &ProjectInfo) -> Result<()> {
let base = project_info.source_dir_path();
let file_path = base.join("main.py");
let file_content = create_main_file(project_info);
save_file_with_content(&file_path, &file_content)?;
Ok(())
}
fn create_types_file(project_info: &ProjectInfo) -> Result<String> {
if is_python_version_or_greater(&project_info.min_python_version, 12)? {
Ok(r#"from typing import Any, Literal
type ActiveFilter = Literal["all", "active", "inactive"]
type Json = dict[str, Any]
"#
.to_string())
} else {
Ok(r#"from typing import Any, Literal, TypeAlias
ActiveFilter: TypeAlias = Literal["all", "active", "inactive"]
Json: TypeAlias = dict[str, Any]
"#
.to_string())
}
}
fn save_types_file(project_info: &ProjectInfo) -> Result<()> {
let base = project_info.source_dir_path();
let file_path = base.join("types.py");
let file_content = create_types_file(project_info)?;
save_file_with_content(&file_path, &file_content)?;
Ok(())
}
fn create_directories(project_info: &ProjectInfo) -> Result<()> {
[
create_api_dir,
create_core_dir,
create_migrations_dir,
create_models_dir,
create_scripts_dir,
create_services_dir,
create_test_dir,
]
.into_par_iter()
.map(|f| f(project_info))
.collect::<Result<Vec<_>, _>>()?;
Ok(())
}
fn create_api_dir(project_info: &ProjectInfo) -> Result<()> {
let src = &project_info.source_dir_path();
let api_dir = src.join("api");
let routes_dir = api_dir.join("routes");
create_dir_all(&routes_dir)?;
save_init_file(&api_dir)?;
save_init_file(&routes_dir)?;
Ok(())
}
fn create_core_dir(project_info: &ProjectInfo) -> Result<()> {
let src = &project_info.source_dir_path();
let core_dir = src.join("core");
create_dir_all(&core_dir)?;
save_init_file(&core_dir)?;
Ok(())
}
fn create_migrations_dir(project_info: &ProjectInfo) -> Result<()> {
let base = project_info.base_dir();
let migrations_dir = base.join("migrations");
create_dir_all(migrations_dir)?;
Ok(())
}
fn create_models_dir(project_info: &ProjectInfo) -> Result<()> {
let src = &project_info.source_dir_path();
let models_dir = src.join("models");
create_dir_all(&models_dir)?;
save_init_file(&models_dir)?;
Ok(())
}
fn create_scripts_dir(project_info: &ProjectInfo) -> Result<()> {
let src = &project_info.base_dir();
let models_dir = src.join("scripts");
create_dir_all(&models_dir)?;
Ok(())
}
fn create_services_dir(project_info: &ProjectInfo) -> Result<()> {
let src = &project_info.source_dir_path();
let services_dir = src.join("services");
let services_db_dir = services_dir.join("db");
let services_cache_dir = services_dir.join("cache");
create_dir_all(&services_db_dir)?;
create_dir_all(&services_cache_dir)?;
save_init_file(&services_dir)?;
save_init_file(&services_db_dir)?;
save_init_file(&services_cache_dir)?;
Ok(())
}
fn create_test_dir(project_info: &ProjectInfo) -> Result<()> {
let src = &project_info.base_dir();
let test_dir = src.join("tests");
let api_dir = test_dir.join("api");
let routes_dir = api_dir.join("routes");
let core_dir = test_dir.join("core");
let models_dir = test_dir.join("models");
let services_dir = test_dir.join("services");
let services_db_dir = services_dir.join("db");
let services_cache_dir = services_dir.join("cache");
create_dir_all(&routes_dir)?;
create_dir_all(&core_dir)?;
create_dir_all(&models_dir)?;
create_dir_all(&services_cache_dir)?;
create_dir_all(&services_db_dir)?;
save_init_file(&api_dir)?;
save_init_file(&core_dir)?;
save_init_file(&models_dir)?;
save_init_file(&routes_dir)?;
save_init_file(&services_dir)?;
save_init_file(&services_db_dir)?;
save_init_file(&services_cache_dir)?;
Ok(())
}
fn save_init_file(path: &Path) -> Result<()> {
let file_path = path.join("__init__.py");
File::create(file_path)?;
Ok(())
}