feat: added initial config

This commit is contained in:
2024-10-10 15:51:41 +02:00
parent 86e82a4d94
commit 5e990a615e
26 changed files with 694 additions and 4 deletions

0
app/__init__.py Normal file
View File

86
app/main.py Normal file
View File

@@ -0,0 +1,86 @@
import logging
import os
from contextlib import asynccontextmanager
from creyPY.fastapi.app import generate_unique_id
from dotenv import load_dotenv
from fastapi import FastAPI, Security
from fastapi.middleware.cors import CORSMiddleware
from fastapi_pagination import add_pagination
from app.services.auth import verify
load_dotenv()
ENV = os.getenv("ENV", "local").lower()
VERSION = os.getenv("VERSION", "Alpha")
@asynccontextmanager
async def lifespan(app: FastAPI):
from app.setup import setup
setup()
# Create initial API key
from creyPY.fastapi.db.session import get_db
from sqlalchemy.orm import Session
from app.models.auth import APIKey
db: Session = next(get_db())
key_obj = db.query(APIKey).filter(APIKey.note == "local_key").one_or_none()
if not key_obj:
db.add(APIKey(note="local_key")) # type: ignore
db.commit()
key_obj = db.query(APIKey).filter(APIKey.note == "local_key").one()
print(f"Local API key: {key_obj.id}")
yield
# App Setup
app = FastAPI(
title="ApiLog API",
description="Tiny service for ingesting logs via POST and querying them via GET.",
version=VERSION,
docs_url="/",
redoc_url=None,
debug=ENV != "prod",
swagger_ui_parameters={
"docExpansion": "list",
"displayOperationId": True,
"defaultModelsExpandDepth": 5,
"defaultModelExpandDepth": 5,
"filter": True,
"displayRequestDuration": True,
"defaultModelRendering": "model",
"persistAuthorization": True,
},
generate_unique_id_function=generate_unique_id,
dependencies=[Security(verify)],
lifespan=lifespan,
)
origins = [
"http://localhost:3000",
"http://localhost:5173",
"http://localhost:4200",
]
# CORS Setup
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# App Routers
from app.routes.app import router as app_router
app.include_router(app_router)
# Pagination
add_pagination(app)

6
app/models/app.py Normal file
View File

@@ -0,0 +1,6 @@
from creyPY.fastapi.models.base import Base
from sqlalchemy import Column, String
class Application(Base):
name = Column(String(512), nullable=False, unique=True)

6
app/models/auth.py Normal file
View File

@@ -0,0 +1,6 @@
from creyPY.fastapi.models.base import Base
from sqlalchemy import Column, String
class APIKey(Base):
note = Column(String(512), nullable=False, unique=True)

0
app/models/entry.py Normal file
View File

65
app/routes/app.py Normal file
View File

@@ -0,0 +1,65 @@
from creyPY.fastapi.crud import (
create_obj_from_data,
)
from creyPY.fastapi.db.session import get_db
from fastapi import APIRouter, Depends, Security, HTTPException
from sqlalchemy.orm import Session
from app.services.auth import verify
from app.schema.app import AppIN, AppOUT
from app.models.app import Application
from fastapi_pagination.ext.sqlalchemy import paginate
from creyPY.fastapi.pagination import Page
from uuid import UUID
router = APIRouter(prefix="/app", tags=["apps"])
@router.post("/", status_code=201)
async def create_app(
data: AppIN,
sub: str = Security(verify),
db: Session = Depends(get_db),
) -> AppOUT:
obj = create_obj_from_data(
data,
Application,
db,
additonal_data={"created_by_id": sub},
)
return AppOUT.model_validate(obj)
@router.delete("/{app_id}", status_code=204)
async def delete_app(
app_id: UUID,
sub: str = Security(verify),
db: Session = Depends(get_db),
) -> None:
obj = db.query(Application).filter_by(id=app_id, created_by_id=sub).one_or_none()
if obj is None:
raise HTTPException(status_code=404, detail="Item not found")
db.delete(obj)
db.commit()
return None
@router.get("/")
async def get_apps(
sub: str = Security(verify),
db: Session = Depends(get_db),
) -> Page[AppOUT]:
the_select = db.query(Application).filter_by(created_by_id=sub)
return paginate(the_select)
@router.get("/{app_id}")
async def get_app(
app_id: UUID,
sub: str = Security(verify),
db: Session = Depends(get_db),
) -> AppOUT:
obj = db.query(Application).filter_by(id=app_id, created_by_id=sub).one_or_none()
if obj is None:
raise HTTPException(status_code=404, detail="Item not found")
return AppOUT.model_validate(obj)

9
app/schema/app.py Normal file
View File

@@ -0,0 +1,9 @@
from app.schema.common import BaseSchemaModelIN, BaseSchemaModelOUT
class AppIN(BaseSchemaModelIN):
name: str
class AppOUT(BaseSchemaModelOUT, AppIN):
pass

10
app/schema/common.py Normal file
View File

@@ -0,0 +1,10 @@
from creyPY.fastapi.schemas.base import BaseSchemaModelOUT as TemplateOUT
from pydantic import BaseModel, ConfigDict
class BaseSchemaModelIN(BaseModel):
model_config = ConfigDict(from_attributes=True)
class BaseSchemaModelOUT(BaseSchemaModelIN, TemplateOUT):
pass

24
app/services/auth.py Normal file
View File

@@ -0,0 +1,24 @@
from uuid import UUID
from creyPY.fastapi.db.session import get_db
from dotenv import load_dotenv
from fastapi import Depends, HTTPException, Request, Security
from fastapi.security import APIKeyQuery
from sqlalchemy.orm import Session
from app.models.auth import APIKey
load_dotenv()
async def verify(
request: Request,
api_key_query: str = Security(APIKeyQuery(name="api-key", auto_error=False)),
db: Session = Depends(get_db),
) -> str:
if api_key_query:
key_info = db.query(APIKey).filter_by(id=UUID(api_key_query)).one_or_none()
if key_info is None:
raise HTTPException(status_code=401, detail="Invalid API key.")
return f"API-KEY: {key_info.note}"
raise HTTPException(status_code=401, detail="No API key.")

View File

@@ -0,0 +1,6 @@
from creyPY.fastapi.models.base import Base # noqa, isort:skip
# custom models from all apps
from app.models.entry import * # noqa, isort:skip
from app.models.auth import * # noqa, isort:skip
from app.models.app import * # noqa, isort:skip

View File

@@ -0,0 +1,8 @@
from sqlalchemy_utils import create_database, database_exists
def create_if_not_exists(db_name: str):
from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL
if not database_exists(SQLALCHEMY_DATABASE_URL + db_name):
create_database(SQLALCHEMY_DATABASE_URL + db_name)

20
app/setup.py Normal file
View File

@@ -0,0 +1,20 @@
import os
from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL, name
from alembic import command
from alembic.config import Config
from app.services.db.session import create_if_not_exists
def setup(db_name=name):
# Create Database
create_if_not_exists(db_name)
# Make alembic migrations
config = Config()
config.set_main_option("sqlalchemy.url", SQLALCHEMY_DATABASE_URL + db_name)
config.set_main_option(
"script_location", os.path.join(os.path.dirname(os.path.dirname(__file__)), "alembic")
)
command.upgrade(config, "head")

51
app/test_main.py Normal file
View File

@@ -0,0 +1,51 @@
from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL, get_db
from creyPY.fastapi.models.base import Base
from creyPY.fastapi.testing import GenericClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy_utils import create_database, database_exists, drop_database
from app.services.auth import verify
from .main import app
CURRENT_USER = "api-key|testing"
class TestAPI:
def setup_class(self):
self.engine = create_engine(SQLALCHEMY_DATABASE_URL + "test", pool_pre_ping=True)
if database_exists(self.engine.url):
drop_database(self.engine.url)
create_database(self.engine.url)
Base.metadata.create_all(self.engine)
def get_db_test():
db = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)()
try:
yield db
finally:
db.close()
def get_test_sub():
global CURRENT_USER
return CURRENT_USER
app.dependency_overrides[get_db] = get_db_test
app.dependency_overrides[verify] = get_test_sub
self.c = GenericClient(app)
def teardown_class(self):
drop_database(self.engine.url)
def test_swagger_gen(self):
re = self.c.get("/openapi.json")
assert re["info"]["title"] == "ApiLog API"
def test_health_check(self):
self.c.get("/", parse_json=False)
def test_application_api(self):
self.c.obj_lifecycle({"name": "Testing"}, "/app/")