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
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
dev.sqlite3
|
||||
TEST_DB/
|
||||
files/
|
||||
DOCKER/
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
@@ -77,6 +81,7 @@ target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
.virtual_documents
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
@@ -106,10 +111,8 @@ ipython_config.py
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
@@ -160,3 +163,6 @@ cython_debug/
|
||||
# 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.
|
||||
#.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
|
||||
|
||||
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