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:
12
.gitignore
vendored
12
.gitignore
vendored
@@ -60,6 +60,10 @@ cover/
|
|||||||
local_settings.py
|
local_settings.py
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
db.sqlite3-journal
|
db.sqlite3-journal
|
||||||
|
dev.sqlite3
|
||||||
|
TEST_DB/
|
||||||
|
files/
|
||||||
|
DOCKER/
|
||||||
|
|
||||||
# Flask stuff:
|
# Flask stuff:
|
||||||
instance/
|
instance/
|
||||||
@@ -77,6 +81,7 @@ target/
|
|||||||
|
|
||||||
# Jupyter Notebook
|
# Jupyter Notebook
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
|
.virtual_documents
|
||||||
|
|
||||||
# IPython
|
# IPython
|
||||||
profile_default/
|
profile_default/
|
||||||
@@ -106,10 +111,8 @@ ipython_config.py
|
|||||||
#pdm.lock
|
#pdm.lock
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
# in version control.
|
# in version control.
|
||||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
.pdm.toml
|
.pdm.toml
|
||||||
.pdm-python
|
|
||||||
.pdm-build/
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
__pypackages__/
|
__pypackages__/
|
||||||
@@ -160,3 +163,6 @@ cython_debug/
|
|||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
# MacOS
|
||||||
|
.DS_Store
|
||||||
|
|||||||
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Run",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "uvicorn",
|
||||||
|
"args": ["app.main:app", "--reload", "--port", "9000"],
|
||||||
|
"jinja": true,
|
||||||
|
"justMyCode": true,
|
||||||
|
"env": {
|
||||||
|
"PYDEVD_DISABLE_FILE_VALIDATION": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Migrate",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "alembic",
|
||||||
|
"args": ["upgrade", "head"],
|
||||||
|
"jinja": true,
|
||||||
|
"justMyCode": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Make Migrations",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "alembic",
|
||||||
|
"args": ["revision", "--autogenerate"],
|
||||||
|
"jinja": true,
|
||||||
|
"justMyCode": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
42
.vscode/settings.json
vendored
Normal file
42
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python3",
|
||||||
|
"python.globalModuleInstallation": false,
|
||||||
|
"python.terminal.activateEnvironment": true,
|
||||||
|
"python.analysis.typeCheckingMode": "basic",
|
||||||
|
"[python]": {
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||||
|
},
|
||||||
|
"black-formatter.args": ["--line-length", "100"],
|
||||||
|
"isort.args": ["--profile", "black"],
|
||||||
|
// Editor General
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"editor.fontSize": 15,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.rulers": [100],
|
||||||
|
"editor.minimap.enabled": true,
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.git": true,
|
||||||
|
"**/.pytest_cache": true,
|
||||||
|
"**/.venv": true,
|
||||||
|
"**/.svn": true,
|
||||||
|
"**/.hg": true,
|
||||||
|
"**/db.sqlite3": true,
|
||||||
|
"**/.DS_Store": true,
|
||||||
|
"**/*.pyc": true,
|
||||||
|
"**/__pycache__/": true
|
||||||
|
},
|
||||||
|
"search.exclude": {
|
||||||
|
"**/.git": true,
|
||||||
|
"**/.venv": true,
|
||||||
|
"**/tmp": true,
|
||||||
|
"htmlcov/*": true,
|
||||||
|
"docs/*": true,
|
||||||
|
".venv/*": true
|
||||||
|
},
|
||||||
|
"python.testing.pytestArgs": ["app"],
|
||||||
|
"python.testing.unittestEnabled": false,
|
||||||
|
"python.testing.pytestEnabled": true
|
||||||
|
}
|
||||||
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
FROM python:3.12-slim AS builder
|
||||||
|
WORKDIR /build
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip wheel --no-cache-dir --wheel-dir=/build/wheels \
|
||||||
|
-r requirements.txt \
|
||||||
|
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y curl git
|
||||||
|
|
||||||
|
COPY --from=builder /build/wheels /wheels
|
||||||
|
RUN pip install --no-cache /wheels/*
|
||||||
|
|
||||||
|
# Remove the wheels directory after installation to save space
|
||||||
|
RUN rm -rf /wheels
|
||||||
|
# Python setup
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
ARG VERSION=unknown
|
||||||
|
ENV VERSION=${VERSION}
|
||||||
|
|
||||||
|
CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-w", "6" , "-b", "0.0.0.0:9000","app.main:app"]
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --retries=5 \
|
||||||
|
CMD curl --fail http://localhost:9000/openapi.json || exit 1
|
||||||
@@ -1 +1,3 @@
|
|||||||
# apilog
|
# apilog
|
||||||
|
|
||||||
|
Tiny logging API server, for taking logs via HTTP POST requests.
|
||||||
|
|||||||
61
alembic.ini
Normal file
61
alembic.ini
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# path to migration scripts
|
||||||
|
script_location = alembic
|
||||||
|
|
||||||
|
# sys.path path, will be prepended to sys.path if present.
|
||||||
|
# defaults to the current working directory.
|
||||||
|
prepend_sys_path = .
|
||||||
|
|
||||||
|
# version path separator; As mentioned above, this is the character used to split
|
||||||
|
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||||
|
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||||
|
# Valid values for version_path_separator are:
|
||||||
|
#
|
||||||
|
# version_path_separator = :
|
||||||
|
# version_path_separator = ;
|
||||||
|
# version_path_separator = space
|
||||||
|
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
||||||
|
|
||||||
|
[post_write_hooks]
|
||||||
|
; This will work from alembic version 1.12 https://alembic.sqlalchemy.org/en/latest/autogenerate.html#basic-post-processor-configuration
|
||||||
|
; hooks = black
|
||||||
|
; black.type = exec
|
||||||
|
; black.executable = black
|
||||||
|
; black.options = -l 100
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
43
alembic/env.py
Normal file
43
alembic/env.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
if config.config_file_name is not None:
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
|
||||||
|
# Our models
|
||||||
|
from app.services.db.models import Base
|
||||||
|
|
||||||
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations() -> None:
|
||||||
|
from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL, name
|
||||||
|
|
||||||
|
with create_engine(SQLALCHEMY_DATABASE_URL + name, pool_pre_ping=True).connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
compare_type=True,
|
||||||
|
compare_server_default=True,
|
||||||
|
render_as_batch=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
run_migrations()
|
||||||
26
alembic/script.py.mako
Normal file
26
alembic/script.py.mako
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = ${repr(up_revision)}
|
||||||
|
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||||
|
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
38
alembic/versions/95201f00f6b9_.py
Normal file
38
alembic/versions/95201f00f6b9_.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 95201f00f6b9
|
||||||
|
Revises: e253d9799d38
|
||||||
|
Create Date: 2024-10-10 15:45:50.089915
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "95201f00f6b9"
|
||||||
|
down_revision: Union[str, None] = "e253d9799d38"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"application",
|
||||||
|
sa.Column("name", sa.String(length=512), nullable=False),
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True
|
||||||
|
),
|
||||||
|
sa.Column("updated_at", sa.DateTime(), nullable=True),
|
||||||
|
sa.Column("created_by_id", sa.String(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id", name="pk_application"),
|
||||||
|
sa.UniqueConstraint("name", name="uq_application_name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table("application")
|
||||||
38
alembic/versions/e253d9799d38_.py
Normal file
38
alembic/versions/e253d9799d38_.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: e253d9799d38
|
||||||
|
Revises:
|
||||||
|
Create Date: 2024-10-10 15:23:32.339647
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "e253d9799d38"
|
||||||
|
down_revision: Union[str, None] = None
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"apikey",
|
||||||
|
sa.Column("note", sa.String(length=512), nullable=False),
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True
|
||||||
|
),
|
||||||
|
sa.Column("updated_at", sa.DateTime(), nullable=True),
|
||||||
|
sa.Column("created_by_id", sa.String(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id", name="pk_apikey"),
|
||||||
|
sa.UniqueConstraint("note", name="uq_apikey_note"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table("apikey")
|
||||||
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/")
|
||||||
36
docker-compose.yml
Normal file
36
docker-compose.yml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
x-restart-policy: &restart_policy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
services:
|
||||||
|
# apilog_worker:
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: Dockerfile
|
||||||
|
# <<: *restart_policy
|
||||||
|
# container_name: api_worker
|
||||||
|
# environment:
|
||||||
|
# - POSTGRES_HOST=apilog_db
|
||||||
|
# - POSTGRES_PORT=5432
|
||||||
|
# - POSTGRES_USER=root
|
||||||
|
# - POSTGRES_PASSWORD=password
|
||||||
|
# - POSTGRES_DB=apilog
|
||||||
|
# depends_on:
|
||||||
|
# apilog_db:
|
||||||
|
# condition: service_healthy
|
||||||
|
|
||||||
|
apilog_db:
|
||||||
|
image: postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: root
|
||||||
|
POSTGRES_DB: fastapi
|
||||||
|
<<: *restart_policy
|
||||||
|
container_name: apilog_db
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U root"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
9
pytest.ini
Normal file
9
pytest.ini
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[pytest]
|
||||||
|
filterwarnings =
|
||||||
|
default:::app.*
|
||||||
|
ignore
|
||||||
|
addopts = -sl
|
||||||
|
log_cli = 1
|
||||||
|
log_cli_level = INFO
|
||||||
|
log_cli_format = %(asctime)s [%(levelname)s] %(message)s (%(filename)s:%(lineno)s)
|
||||||
|
log_cli_date_format=%H:%M:%S
|
||||||
34
requirements.txt
Normal file
34
requirements.txt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
annotated-types==0.7.0
|
||||||
|
anyio==4.6.0
|
||||||
|
certifi==2024.8.30
|
||||||
|
creyPY==1.2.5
|
||||||
|
fastapi==0.115.0
|
||||||
|
fastapi-pagination==0.12.31
|
||||||
|
h11==0.14.0
|
||||||
|
httpcore==1.0.6
|
||||||
|
httpx==0.27.2
|
||||||
|
idna==3.10
|
||||||
|
psycopg==3.2.3
|
||||||
|
psycopg-binary==3.2.3
|
||||||
|
psycopg-pool==3.2.3
|
||||||
|
pydantic==2.9.2
|
||||||
|
pydantic_core==2.23.4
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
sniffio==1.3.1
|
||||||
|
SQLAlchemy==2.0.35
|
||||||
|
starlette==0.38.6
|
||||||
|
typing_extensions==4.12.2
|
||||||
|
|
||||||
|
Mako==1.3.5 # Alembic
|
||||||
|
MarkupSafe==3.0.1 # Alembic
|
||||||
|
alembic==1.13.3 # Alembic
|
||||||
|
|
||||||
|
SQLAlchemy-Utils==0.41.2 # SQLAlchemy
|
||||||
|
|
||||||
|
click==8.1.7 # Uvicorn
|
||||||
|
uvicorn==0.31.1 # Uvicorn
|
||||||
|
|
||||||
|
iniconfig==2.0.0 # pytest
|
||||||
|
packaging==24.1 # pytest
|
||||||
|
pluggy==1.5.0 # pytest
|
||||||
|
pytest==8.3.3 # pytest
|
||||||
Reference in New Issue
Block a user