Compare commits

..

2 Commits

Author SHA1 Message Date
renovate[bot]
aed4cc21f7 Merge 5d9103960d into e77fe115c6 2024-11-24 20:16:23 +00:00
renovate[bot]
5d9103960d feat(deps): update dependency mako to v1.3.6 2024-11-24 20:16:20 +00:00
13 changed files with 98 additions and 208 deletions

View File

@@ -5,7 +5,6 @@ on:
branches:
- dev
- master
- renovate/**
paths-ignore:
- "**/.github/**"
- "**/.gitignore"

View File

@@ -1,4 +1,4 @@
FROM python:3.13-slim
FROM python:3.12-slim
ARG VERSION=unknown
# Create a non-root user and group

View File

@@ -1,8 +1,3 @@
# apilog
Tiny logging API server, for taking logs via HTTP POST requests.
## TODO
[ ] Application Patch
[ ] Team CRUD

View File

@@ -1,29 +0,0 @@
"""empty message
Revision ID: 1e695b024786
Revises: 21dc1dc045b8
Create Date: 2025-01-20 11:36:14.692849
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "1e695b024786"
down_revision: Union[str, None] = "21dc1dc045b8"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
with op.batch_alter_table("application", schema=None) as batch_op:
batch_op.add_column(sa.Column("retention_days", sa.Integer(), nullable=True))
def downgrade() -> None:
with op.batch_alter_table("application", schema=None) as batch_op:
batch_op.drop_column("retention_days")

View File

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

View File

@@ -1,9 +1,9 @@
from enum import Enum as pyenum
from creyPY.fastapi.models.base import Base
from sqlalchemy import JSON, Column, Enum, ForeignKey, String
from sqlalchemy import Column, String, ForeignKey, Enum, JSON
from sqlalchemy.dialects.postgresql import UUID
from enum import Enum as pyenum
class TransactionType(pyenum):
CREATE = "create"

View File

@@ -1,16 +1,17 @@
from uuid import UUID
from creyPY.fastapi.crud import create_obj_from_data
from creyPY.fastapi.crud import (
create_obj_from_data,
)
from creyPY.fastapi.db.session import get_db
from creyPY.fastapi.pagination import Page, paginate
from fastapi import APIRouter, Depends, HTTPException, Security
from pydantic.json_schema import SkipJsonSchema
from sqlalchemy import select
from fastapi import APIRouter, Depends, Security, HTTPException
from sqlalchemy.orm import Session
from app.models.app import Application
from app.schema.app import AppIN, AppOUT
from pydantic.json_schema import SkipJsonSchema
from app.services.auth import verify
from app.schema.app import AppIN, AppOUT
from app.models.app import Application
from creyPY.fastapi.pagination import Page
from uuid import UUID
from fastapi_pagination.ext.sqlalchemy import paginate
from sqlalchemy import select
router = APIRouter(prefix="/app", tags=["apps"])
@@ -25,7 +26,7 @@ async def create_app(
data,
Application,
db,
additional_data={"created_by_id": sub},
additonal_data={"created_by_id": sub},
)
return AppOUT.model_validate(obj)

View File

@@ -1,22 +1,24 @@
from datetime import datetime
from typing import Callable
from uuid import UUID
from creyPY.fastapi.crud import create_obj_from_data
from creyPY.fastapi.db.session import get_db
from creyPY.fastapi.crud import (
create_obj_from_data,
)
from creyPY.fastapi.order_by import order_by
from creyPY.fastapi.pagination import Page, paginate
from fastapi import APIRouter, Depends, HTTPException, Security
from typing import Callable
from sqlalchemy.sql.selectable import Select
from creyPY.fastapi.db.session import get_db
from fastapi import APIRouter, Depends, Security, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import select
from app.services.auth import verify
from app.schema.entry import LogIN, LogOUT
from app.models.entry import LogEntry
from fastapi_pagination.ext.sqlalchemy import paginate
from creyPY.fastapi.pagination import Page
from uuid import UUID
from pydantic.json_schema import SkipJsonSchema
from fastapi_filters import FilterValues, create_filters
from fastapi_filters.ext.sqlalchemy import apply_filters
from pydantic.json_schema import SkipJsonSchema
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.sql.selectable import Select
from app.models.entry import LogEntry, LogType, TransactionType
from app.schema.entry import LogIN, LogOUT
from app.services.auth import verify
from app.models.entry import LogType, TransactionType
from datetime import datetime
router = APIRouter(prefix="/log", tags=["logging"])
@@ -31,7 +33,7 @@ async def create_log(
data,
LogEntry,
db,
additional_data={"created_by_id": sub},
additonal_data={"created_by_id": sub},
)
return LogOUT.model_validate(obj)

View File

@@ -1,11 +1,8 @@
from pydantic.json_schema import SkipJsonSchema
from app.schema.common import BaseSchemaModelIN, BaseSchemaModelOUT
class AppIN(BaseSchemaModelIN):
name: str
retention_days: int | SkipJsonSchema[None] = 30
class AppOUT(BaseSchemaModelOUT, AppIN):

View File

@@ -1,32 +1,12 @@
import os
from datetime import datetime, timedelta
from apscheduler.schedulers.background import BackgroundScheduler
from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL, get_db, name
from sqlalchemy.orm import Session
from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL, name
from alembic import command
from alembic.config import Config
from app.models.app import Application
from app.models.entry import LogEntry
from app.services.db.session import create_if_not_exists
def delete_old_logs(sess: Session | None = None):
session = sess or next(get_db())
for app in session.query(Application).filter(Application.retention_days.isnot(None)):
cutoff = datetime.now() - timedelta(days=app.retention_days)
print(
f"Deleting logs older than {app.retention_days} days (cutoff: {cutoff}) for {app.name}",
)
session.query(LogEntry).filter(
LogEntry.application == app.id, LogEntry.created_at < cutoff
).delete()
session.commit()
def setup(db_name=name):
# Create Database
create_if_not_exists(db_name)
@@ -38,17 +18,3 @@ def setup(db_name=name):
"script_location", os.path.join(os.path.dirname(os.path.dirname(__file__)), "alembic")
)
command.upgrade(config, "head")
# Start retention deletion
scheduler = BackgroundScheduler()
scheduler.add_job(
delete_old_logs,
"interval",
id="deletor",
days=1,
max_instances=1,
replace_existing=True,
next_run_time=datetime.now(),
)
scheduler.start()
print("Deletion scheduler started")

View File

@@ -1,6 +1,3 @@
import contextlib
from datetime import datetime, timedelta
from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL, get_db
from creyPY.fastapi.models.base import Base
from creyPY.fastapi.testing import GenericClient
@@ -8,55 +5,16 @@ from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy_utils import create_database, database_exists, drop_database
from app.models.entry import LogEntry
from app.services.auth import verify
from app.setup import delete_old_logs
import contextlib
from .main import app
CURRENT_USER = "api-key|testing"
ENTRY_EXAMPLES = [
{
"l_type": "info",
"t_type": "create",
"message": "User Max Mustermann created",
"environment": "dev",
},
{
"l_type": "info",
"t_type": "update",
"message": "User Max Mustermann updated",
"environment": "dev",
},
{
"l_type": "info",
"t_type": "create",
"author": "auth|max_muster",
"message": "User Max Mustermann created a Unit",
"object_reference": "1",
"environment": "dev",
},
{
"l_type": "info",
"t_type": "update",
"author": "auth|max_muster",
"message": "User Max Mustermann updated Unit 1",
"object_reference": "1",
"previous_object": {"name": "Unit 1"},
"environment": "prod",
},
{
"l_type": "warning",
"t_type": "delete",
"message": "User Max Mustermann deleted",
"environment": "prod",
},
]
@contextlib.contextmanager
def app_context(self, name: str = "Testing", retention_days: int | None = None):
app_id = self.create_app(name, retention_days)
def app_context(self, name: str = "Testing"):
app_id = self.create_app(name)
try:
yield app_id
finally:
@@ -65,8 +23,45 @@ def app_context(self, name: str = "Testing", retention_days: int | None = None):
@contextlib.contextmanager
def log_examples(self):
LOG_EXAMPLES = [
{
"l_type": "info",
"t_type": "create",
"message": "User Max Mustermann created",
"environment": "dev",
},
{
"l_type": "info",
"t_type": "update",
"message": "User Max Mustermann updated",
"environment": "dev",
},
{
"l_type": "info",
"t_type": "create",
"author": "auth|max_muster",
"message": "User Max Mustermann created a Unit",
"object_reference": "1",
"environment": "dev",
},
{
"l_type": "info",
"t_type": "update",
"author": "auth|max_muster",
"message": "User Max Mustermann updated Unit 1",
"object_reference": "1",
"previous_object": {"name": "Unit 1"},
"environment": "prod",
},
{
"l_type": "warning",
"t_type": "delete",
"message": "User Max Mustermann deleted",
"environment": "prod",
},
]
with app_context(self) as app_id:
for entry in ENTRY_EXAMPLES:
for entry in LOG_EXAMPLES:
self.log_message({"application": app_id, **entry})
yield app_id
@@ -91,7 +86,6 @@ class TestAPI:
global CURRENT_USER
return CURRENT_USER
self.db_instance = get_db_test()
app.dependency_overrides[get_db] = get_db_test
app.dependency_overrides[verify] = get_test_sub
self.c = GenericClient(app)
@@ -100,8 +94,8 @@ class TestAPI:
drop_database(self.engine.url)
# HELPERS
def create_app(self, name: str = "Testing", retention_days: int | None = None):
re = self.c.post("/app/", {"name": name, "retention_days": retention_days})
def create_app(self, name: str = "Testing"):
re = self.c.post("/app/", {"name": name})
return re["id"]
def destroy_app(self, app_id):
@@ -266,29 +260,3 @@ class TestAPI:
re = self.c.get("/log/?application=" + str(app_id))
assert re["total"] == 0
def test_retention_delete(self):
sess = next(self.db_instance)
with app_context(self, retention_days=2) as app_id:
for i in range(5):
sess.add(
LogEntry(
application=app_id,
created_at=datetime.now() - timedelta(days=i),
created_by_id=CURRENT_USER,
)
)
sess.commit()
assert sess.query(LogEntry).count() == 5
re = self.c.get("/log/?application=" + str(app_id))
assert re["total"] == 5
delete_old_logs(sess)
assert sess.query(LogEntry).count() == 2
# delete all logs
re = self.c.delete("/log/?application=" + str(app_id), r_code=200)

View File

@@ -1,12 +1,7 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended", ":semanticCommitTypeAll(feat)"],
"packageRules": [
{
"automerge": true,
"description": "Automerge non-major updates",
"matchUpdateTypes": ["minor", "patch"],
"automergeType": "branch"
}
"extends": [
"config:recommended",
":semanticCommitTypeAll(feat)"
]
}

View File

@@ -1,39 +1,36 @@
annotated-types==0.7.0
anyio==4.9.0
certifi==2025.1.31
creyPY[postgres]==3.0.0
fastapi==0.115.11
anyio==4.6.2.post1
certifi==2024.8.30
creyPY==1.2.5
fastapi==0.115.5
fastapi-pagination==0.12.31
h11==0.14.0
httpcore==1.0.6
httpx==0.28.1
httpx==0.27.2
idna==3.10
psycopg==3.2.6
psycopg-binary==3.2.6
psycopg-pool==3.2.6
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.39
starlette==0.46.1
SQLAlchemy==2.0.35
starlette==0.40.0
typing_extensions==4.12.2
Mako==1.3.9 # Alembic
Mako==1.3.6 # Alembic
MarkupSafe==3.0.1 # Alembic
alembic==1.15.1 # Alembic
alembic==1.13.3 # Alembic
SQLAlchemy-Utils==0.41.2 # SQLAlchemy
click==8.1.8 # Uvicorn
uvicorn==0.34.0 # Uvicorn
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.5 # pytest
pytest==8.3.3 # pytest
fastapi-filters==0.2.9 # Filters
APScheduler==3.11.0 # Scheduler for deletion
tzlocal==5.3.1 # Scheduler for deletion