mirror of
https://github.com/creyD/apilog.git
synced 2026-04-12 19:30:29 +02:00
feat: added initial config
This commit is contained in:
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
86
app/main.py
Normal file
86
app/main.py
Normal 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
6
app/models/app.py
Normal 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
6
app/models/auth.py
Normal 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
0
app/models/entry.py
Normal file
65
app/routes/app.py
Normal file
65
app/routes/app.py
Normal 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
9
app/schema/app.py
Normal 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
10
app/schema/common.py
Normal 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
24
app/services/auth.py
Normal 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.")
|
||||
6
app/services/db/models.py
Normal file
6
app/services/db/models.py
Normal 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
|
||||
8
app/services/db/session.py
Normal file
8
app/services/db/session.py
Normal 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
20
app/setup.py
Normal 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
51
app/test_main.py
Normal 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/")
|
||||
Reference in New Issue
Block a user