Initial commit
This commit is contained in:
commit
85d261e987
18 files changed
+709
No files matched your search
@@ -0,0 +1,63 @@
|
||||
name: Build & Publish to GHCR via local build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GHCR_ORG: gansejunge
|
||||
IMAGE_NAME: app-notifications-service-GENERIC
|
||||
IMAGE_PATH: ghcr.io/gansejunge/app-notifications-service-GENERIC
|
||||
OPS_ORG: notifier
|
||||
OPS_REPO: ops-deployment
|
||||
OPS_BRANCH_BASE: main
|
||||
OPS_PATH: service-GENERIC/deployment.yaml
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: local
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GHCR
|
||||
run: echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ secrets.GHCR_USERNAME }} --password-stdin
|
||||
|
||||
- name: Get Latest Tag
|
||||
id: get_tag
|
||||
run: |
|
||||
BASE64_TOKEN=$(echo "${{ secrets.GHCR_TOKEN }}" | base64)
|
||||
LATEST_TAG=$(curl -s -H "Authorization: Bearer $BASE64_TOKEN" \
|
||||
https://ghcr.io/v2/${{ env.GHCR_ORG }}/${{ env.IMAGE_NAME }}/tags/list \
|
||||
| jq -r '.tags | map(select(test("^[0-9]+$"))) | map(tonumber) | max // 0')
|
||||
NEXT_TAG=$((LATEST_TAG + 1))
|
||||
echo "latest=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "next=$NEXT_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
run: |
|
||||
docker build \
|
||||
--build-arg COMMIT_ID=$(echo "${{ github.sha }}" | cut -c1-7) \
|
||||
-t ghcr.io/gansejunge/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.next }} .
|
||||
docker push ghcr.io/gansejunge/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.next }}
|
||||
|
||||
- name: Clone ops-deployment repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: notifier/ops-deployment
|
||||
path: ./ops-deployment
|
||||
token: ${{ secrets.BOT_TOKEN }}
|
||||
|
||||
- name: Update GENERIC deployment
|
||||
working-directory: ./ops-deployment
|
||||
run: |
|
||||
NEW_TAG=${{ steps.get_tag.outputs.next }}
|
||||
NEW_IMAGE="${{ env.IMAGE_PATH }}:$NEW_TAG"
|
||||
git config user.name "automation-bot"
|
||||
git config user.email "dev@gansejunge.com"
|
||||
|
||||
sed -i "s|ghcr.io/$GHCR_ORG/$IMAGE_NAME:[0-9]\+|$NEW_IMAGE|g" "$OPS_PATH"
|
||||
|
||||
COMMIT_URL="https://git.gansejunge.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}"
|
||||
git add $OPS_PATH
|
||||
git commit -m "Update GENERIC image to version $NEW_TAG" -m "Linked build commit: $COMMIT_URL"
|
||||
git push origin $OPS_BRANCH_BASE
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Build & Publish to GHCR
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GHCR_ORG: gansejunge
|
||||
IMAGE_NAME: app-notifications-service-GENERIC
|
||||
IMAGE_PATH: ghcr.io/gansejunge/app-notifications-service-GENERIC
|
||||
OPS_ORG: notifier
|
||||
OPS_REPO: ops-deployment
|
||||
OPS_BRANCH_BASE: main
|
||||
OPS_PATH: service-GENERIC/deployment.yaml
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GHCR
|
||||
run: echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ secrets.GHCR_USERNAME }} --password-stdin
|
||||
|
||||
- name: Get Latest Tag
|
||||
id: get_tag
|
||||
run: |
|
||||
BASE64_TOKEN=$(echo "${{ secrets.GHCR_TOKEN }}" | base64)
|
||||
LATEST_TAG=$(curl -s -H "Authorization: Bearer $BASE64_TOKEN" \
|
||||
https://ghcr.io/v2/${{ env.GHCR_ORG }}/${{ env.IMAGE_NAME }}/tags/list \
|
||||
| jq -r '.tags | map(select(test("^[0-9]+$"))) | map(tonumber) | max // 0')
|
||||
NEXT_TAG=$((LATEST_TAG + 1))
|
||||
echo "latest=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "next=$NEXT_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
run: |
|
||||
docker build \
|
||||
--build-arg COMMIT_ID=$(echo "${{ github.sha }}" | cut -c1-7) \
|
||||
-t ghcr.io/gansejunge/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.next }} .
|
||||
docker push ghcr.io/gansejunge/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.next }}
|
||||
|
||||
- name: Clone ops-deployment repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: notifier/ops-deployment
|
||||
path: ./ops-deployment
|
||||
token: ${{ secrets.BOT_TOKEN }}
|
||||
|
||||
- name: Update GENERIC deployment
|
||||
working-directory: ./ops-deployment
|
||||
run: |
|
||||
NEW_TAG=${{ steps.get_tag.outputs.next }}
|
||||
NEW_IMAGE="${{ env.IMAGE_PATH }}:$NEW_TAG"
|
||||
git config user.name "automation-bot"
|
||||
git config user.email "dev@gansejunge.com"
|
||||
|
||||
sed -i "s|ghcr.io/$GHCR_ORG/$IMAGE_NAME:[0-9]\+|$NEW_IMAGE|g" "$OPS_PATH"
|
||||
|
||||
COMMIT_URL="https://git.gansejunge.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}"
|
||||
git add $OPS_PATH
|
||||
git commit -m "Update GENERIC image to version $NEW_TAG" -m "Linked build commit: $COMMIT_URL"
|
||||
git push origin $OPS_BRANCH_BASE
|
||||
@@ -0,0 +1,78 @@
|
||||
name: pip-audit
|
||||
on:
|
||||
schedule:
|
||||
- cron: "35 1 * * *"
|
||||
push:
|
||||
branches: [main]
|
||||
paths: [requirements.txt, scripts/filter_requirements.py]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
report_b64: ${{ steps.audit.outputs.report_b64 }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Audit dependencies
|
||||
id: audit
|
||||
shell: bash
|
||||
run: |
|
||||
pip install pip-audit
|
||||
python3 scripts/filter_requirements.py requirements.txt requirements.audit.txt
|
||||
|
||||
# Don't let a non-zero exit abort the step, so the report is saved first
|
||||
set +e
|
||||
pip-audit -r requirements.audit.txt --no-deps --disable-pip > audit.txt 2>&1
|
||||
echo $? > audit.exit
|
||||
set -e
|
||||
|
||||
cat audit.txt
|
||||
echo "report_b64=$(head -c 100000 audit.txt | base64 -w0)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Fail if the audit failed
|
||||
shell: bash
|
||||
run: exit "$(cat audit.exit)"
|
||||
|
||||
notify:
|
||||
needs: audit
|
||||
if: ${{ always() && needs.audit.result == 'failure' }}
|
||||
runs-on: host
|
||||
steps:
|
||||
- name: Send mail via local postfix
|
||||
env:
|
||||
MAIL_FROM: ${{ vars.MAIL_FROM }}
|
||||
MAIL_TO: ${{ vars.MAIL_TO }}
|
||||
REPO: ${{ gitea.repository }}
|
||||
RUN_URL: ${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_number }}
|
||||
REPORT_B64: ${{ needs.audit.outputs.report_b64 }}
|
||||
run: |
|
||||
python3 - <<'EOF'
|
||||
import base64, os, smtplib
|
||||
from email.message import EmailMessage
|
||||
|
||||
b64 = os.environ.get("REPORT_B64", "")
|
||||
report = (
|
||||
base64.b64decode(b64).decode("utf-8", errors="replace")
|
||||
if b64 else
|
||||
"(no audit output, the job failed before pip-audit finished)"
|
||||
)
|
||||
|
||||
msg = EmailMessage()
|
||||
msg["Subject"] = f"[pip-audit] Problem in {os.environ['REPO']}"
|
||||
msg["From"] = os.environ["MAIL_FROM"]
|
||||
msg["To"] = os.environ["MAIL_TO"]
|
||||
msg.set_content(
|
||||
f"pip-audit result for {os.environ['REPO']}:\n\n"
|
||||
f"{report}\n\n"
|
||||
f"Run: {os.environ['RUN_URL']}\n"
|
||||
)
|
||||
|
||||
with smtplib.SMTP("127.0.0.1", 25, timeout=30) as s:
|
||||
s.send_message(msg)
|
||||
EOF
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#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
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# 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/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Builder stage
|
||||
FROM python:3.12-slim AS builder
|
||||
WORKDIR /install
|
||||
COPY requirements.txt .
|
||||
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt
|
||||
WORKDIR /app
|
||||
COPY src/ /app/
|
||||
|
||||
# Runtime image
|
||||
FROM nvcr.io/nvidia/distroless/python:3.12-v4.2.0
|
||||
COPY --from=builder /install /usr/local
|
||||
COPY --from=builder /app /app
|
||||
WORKDIR /app
|
||||
|
||||
ARG COMMIT_ID=unknown
|
||||
ENV COMMIT_ID=$COMMIT_ID
|
||||
|
||||
ENTRYPOINT ["python", "main.py"]
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py312"
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", "F", # pycodestyle errors, pyflakes (unused imports/variables, undefined names)
|
||||
"I", # import sorting (replaces isort)
|
||||
"B", # bugbear: likely bugs
|
||||
"ASYNC", # async mistakes
|
||||
"RUF006", # un-referenced asyncio.create_task (tasks can be garbage-collected)
|
||||
]
|
||||
ignore = ["E501"]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["db_module", "secret_manager", "service_auth", "shared_models",
|
||||
"simple_logger_handler", "send_notifications", "send_notifications_webhook",]
|
||||
|
||||
|
||||
[tool.ruff.format]
|
||||
indent-style = "tab"
|
||||
|
||||
[tool.ruff.lint.flake8-bugbear]
|
||||
extend-immutable-calls = [
|
||||
"fastapi.Depends",
|
||||
"fastapi.Query",
|
||||
"fastapi.Header",
|
||||
"fastapi.Body",
|
||||
"fastapi.Security",
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
ruff
|
||||
@@ -0,0 +1,11 @@
|
||||
--extra-index-url https://git.gansejunge.com/api/packages/notifier/pypi/simple/
|
||||
fastapi
|
||||
lib-db-module
|
||||
lib-secret-manager
|
||||
lib-send-notifications
|
||||
lib-service-auth
|
||||
lib-shared-models
|
||||
prometheus_client
|
||||
pydantic
|
||||
simple-logger-handler
|
||||
uvicorn
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write a copy of requirements.txt without the self-hosted packages.
|
||||
|
||||
Usage: filter_requirements.py <input> <output>
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
|
||||
SKIP = {
|
||||
"lib-db-module",
|
||||
"lib-secret-manager",
|
||||
"lib-send-notifications",
|
||||
"send_notifications_webhook",
|
||||
"lib-service-auth",
|
||||
"lib-shared-models",
|
||||
"simple-logger-handler",
|
||||
}
|
||||
|
||||
NAME_RE = re.compile(r"^\s*([A-Za-z0-9][A-Za-z0-9._-]*)")
|
||||
|
||||
|
||||
def main(src: str, dst: str) -> None:
|
||||
skipped = []
|
||||
|
||||
with open(src) as fin, open(dst, "w") as fout:
|
||||
for line in fin:
|
||||
match = NAME_RE.match(line)
|
||||
if match and match.group(1) in SKIP:
|
||||
skipped.append(match.group(1))
|
||||
continue
|
||||
fout.write(line)
|
||||
|
||||
print(f"Skipped {len(skipped)} self-hosted package(s): {', '.join(skipped) or '-'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
sys.exit(__doc__)
|
||||
main(sys.argv[1], sys.argv[2])
|
||||
@@ -0,0 +1,13 @@
|
||||
from enum import StrEnum
|
||||
|
||||
from service_auth import KeySource, ServiceAuth
|
||||
|
||||
|
||||
class Caller(StrEnum):
|
||||
GATEWAY = "gateway"
|
||||
SCHEDULER = "scheduler"
|
||||
|
||||
auth = ServiceAuth({
|
||||
Caller.GATEWAY: KeySource("GATEWAY_KEY", "/etc/secrets/gateway_key"),
|
||||
Caller.SCHEDULER: KeySource("SCHEDULER_KEY", "/etc/secrets/scheduler_key"),
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
from metrics_server import REQUEST_COUNTER
|
||||
from simple_logger_handler import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
async def custom_http_exception_handler(request,exc):
|
||||
logger.debug(f"Handling HTTP exception: {exc}")
|
||||
if exc.status_code == 404:
|
||||
return JSONResponse(
|
||||
status_code=403,
|
||||
content={"detail": "Unauthorized"}
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"detail": exc.detail}
|
||||
)
|
||||
|
||||
async def prometheus_middleware(request: Request, call_next):
|
||||
logger.debug(
|
||||
"Incoming request: %s %s",
|
||||
request.method, request.url.path,
|
||||
)
|
||||
status = 500
|
||||
try:
|
||||
response = await call_next(request)
|
||||
status = response.status_code
|
||||
logger.debug("Request processed with status: %s", status)
|
||||
except Exception as e:
|
||||
logger.exception("[FastAPI] Exception during request processing: %s", e)
|
||||
raise
|
||||
finally:
|
||||
REQUEST_COUNTER.labels(request.method, request.url.path, status).inc()
|
||||
logger.debug(
|
||||
"[FastAPI] Prometheus counter incremented: %s %s %s",
|
||||
request.method, request.url.path, status,
|
||||
)
|
||||
return response
|
||||
|
||||
def register_handlers(app: FastAPI) -> None:
|
||||
"""Attach the error handlers and the metrics middleware to the app."""
|
||||
app.add_exception_handler(StarletteHTTPException, custom_http_exception_handler)
|
||||
app.add_middleware(BaseHTTPMiddleware, dispatch=prometheus_middleware)
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import asyncio
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import uvicorn
|
||||
from fastapi import Depends, FastAPI
|
||||
|
||||
from auth_config import Caller, auth
|
||||
from db_module import (
|
||||
close_connection_pool,
|
||||
create_connection_pool,
|
||||
start_healthcheck_thread,
|
||||
)
|
||||
from handlers import register_handlers
|
||||
from metrics_server import metrics_api
|
||||
from routers.scheduler import scheduler_router
|
||||
from routers.user import user_router
|
||||
from secret_manager import cleanup_secret_files
|
||||
from send_notifications import NotificationClient
|
||||
from simple_logger_handler import LOG_LEVEL, setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
logger.info("[App] Starting application...")
|
||||
app.state.notifier = NotificationClient()
|
||||
logger.info("[DB] Creating MySQL connection pool...")
|
||||
create_connection_pool("seadexPool")
|
||||
|
||||
start_healthcheck_thread()
|
||||
logger.info("[DB] MySQL healthcheck thread started.")
|
||||
|
||||
SECRET_PATHS = frozenset({
|
||||
"/etc/secrets/api_key",
|
||||
"/etc/secrets/db_username",
|
||||
"/etc/secrets/db_password"
|
||||
})
|
||||
cleanup_secret_files(SECRET_PATHS)
|
||||
|
||||
yield
|
||||
logger.info("[App] Closing MySQL connection pool...")
|
||||
close_connection_pool()
|
||||
logger.info("[App] Shutdown complete.")
|
||||
|
||||
|
||||
|
||||
api = FastAPI(
|
||||
title="Service GENERIC",
|
||||
description="GENERICGENERICGENERICGENERICGENERIC",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan,
|
||||
dependencies=[Depends(auth.authenticate)],
|
||||
)
|
||||
|
||||
register_handlers(api)
|
||||
api.include_router(user_router)
|
||||
api.include_router(scheduler_router)
|
||||
|
||||
|
||||
async def start_servers():
|
||||
logger.info("[Server] Starting main API (port 5000) and metrics server (port 9000)...")
|
||||
logger.info("[Server] Build commit id=%s", os.getenv("COMMIT_ID", "unknown"))
|
||||
|
||||
auth.load_keys(api, Caller.GATEWAY, Caller.SCHEDULER, Caller.ADMIN)
|
||||
cleanup_secret_files(auth.key_paths(Caller.GATEWAY, Caller.SCHEDULER, Caller.ADMIN))
|
||||
|
||||
config_main = uvicorn.Config(api, host="0.0.0.0", port=5000, log_level=LOG_LEVEL.lower(),log_config=None)
|
||||
config_metrics = uvicorn.Config(metrics_api, host="0.0.0.0", port=9000, log_level=LOG_LEVEL.lower(),log_config=None)
|
||||
|
||||
server_main = uvicorn.Server(config_main)
|
||||
server_metrics = uvicorn.Server(config_metrics)
|
||||
|
||||
await asyncio.gather(server_main.serve(), server_metrics.serve())
|
||||
logger.info("[Server] Both servers have stopped.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(start_servers())
|
||||
@@ -0,0 +1,10 @@
|
||||
from fastapi import FastAPI, Response
|
||||
from prometheus_client import CONTENT_TYPE_LATEST, Counter, generate_latest
|
||||
|
||||
metrics_api = FastAPI(title="Metrics Server", description="Prometheus metrics endpoint")
|
||||
|
||||
REQUEST_COUNTER = Counter("http_requests_total", "Total HTTP Requests", ["method", "endpoint", "status"])
|
||||
|
||||
@metrics_api.get("/metrics")
|
||||
async def metrics():
|
||||
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)
|
||||
@@ -0,0 +1,22 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field, computed_field
|
||||
|
||||
|
||||
class GenericEntry(BaseModel):
|
||||
name: str
|
||||
custom_name: str | None = None
|
||||
published: datetime | None = None
|
||||
|
||||
class Counts(BaseModel):
|
||||
active: int
|
||||
inactive: int
|
||||
|
||||
class GenericList(BaseModel):
|
||||
active: list[GenericEntry] = Field(default_factory=list)
|
||||
inactive: list[GenericEntry] = Field(default_factory=list)
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def counts(self) -> Counts:
|
||||
return Counts(active=len(self.active),inactive=len(self.inactive))
|
||||
Whitespace-only changes.
@@ -0,0 +1,16 @@
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
from auth_config import Caller, auth
|
||||
from db_module import (
|
||||
get_db,
|
||||
)
|
||||
from simple_logger_handler import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
scheduler_router = APIRouter(dependencies=[Depends(auth.require(Caller.SCHEDULER))])
|
||||
|
||||
@scheduler_router.post("/update")
|
||||
def check_for_updates(request:Request, db=Depends(get_db)):
|
||||
return
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from auth_config import Caller, auth
|
||||
from db_module import (
|
||||
get_db,
|
||||
)
|
||||
from models import GenericList
|
||||
from service_auth import UserId
|
||||
from simple_logger_handler import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
user_router = APIRouter(dependencies=[Depends(auth.require(Caller.GATEWAY))])
|
||||
|
||||
@user_router.post("/entry/add")
|
||||
def add_entry(
|
||||
user_id: UserId,
|
||||
db=Depends(get_db),
|
||||
):
|
||||
return
|
||||
|
||||
@user_router.post("/entry/delete")
|
||||
def remove_anime(
|
||||
user_id: UserId,
|
||||
db=Depends(get_db)
|
||||
):
|
||||
return
|
||||
|
||||
@user_router.get("/entry/list", response_model=GenericList)
|
||||
def list_active_and_inactive_anime(
|
||||
user_id: UserId,
|
||||
db=Depends(get_db),
|
||||
):
|
||||
return
|
||||
|
||||
@user_router.post("/entry/rename")
|
||||
def rename_entry(
|
||||
user_id: UserId,
|
||||
db=Depends(get_db),
|
||||
):
|
||||
return
|
||||
Reference in new issue
Block a user