From 85d261e9871c4422990dea18c2c47f6a92175221 Mon Sep 17 00:00:00 2001
From: notifier <>
Date: Wed, 11 Nov 2026 09:56:43 +0000
Subject: [PATCH] Initial commit
---
.gitea/workflows/build-local.yaml | 63 +++++++++++
.gitea/workflows/build.yaml | 63 +++++++++++
.gitea/workflows/pip-audit.yaml | 78 +++++++++++++
.gitignore | 176 ++++++++++++++++++++++++++++++
Dockerfile | 19 ++++
README.md | 2 +
pyproject.toml | 29 +++++
requirements-dev.txt | 1 +
requirements.txt | 11 ++
scripts/filter_requirements.py | 39 +++++++
src/auth_config.py | 13 +++
src/handlers.py | 47 ++++++++
src/main.py | 78 +++++++++++++
src/metrics_server.py | 10 ++
src/models.py | 22 ++++
src/routers/__init__.py | 0
src/routers/scheduler.py | 16 +++
src/routers/user.py | 42 +++++++
18 files changed, 709 insertions(+)
create mode 100644 .gitea/workflows/build-local.yaml
create mode 100644 .gitea/workflows/build.yaml
create mode 100644 .gitea/workflows/pip-audit.yaml
create mode 100644 .gitignore
create mode 100644 Dockerfile
create mode 100644 README.md
create mode 100644 pyproject.toml
create mode 100644 requirements-dev.txt
create mode 100644 requirements.txt
create mode 100644 scripts/filter_requirements.py
create mode 100644 src/auth_config.py
create mode 100644 src/handlers.py
create mode 100644 src/main.py
create mode 100644 src/metrics_server.py
create mode 100644 src/models.py
create mode 100644 src/routers/__init__.py
create mode 100644 src/routers/scheduler.py
create mode 100644 src/routers/user.py
diff --git a/.gitea/workflows/build-local.yaml b/.gitea/workflows/build-local.yaml
new file mode 100644
index 0000000..47a0931
--- /dev/null
+++ b/.gitea/workflows/build-local.yaml
@@ -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
diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml
new file mode 100644
index 0000000..81cc554
--- /dev/null
+++ b/.gitea/workflows/build.yaml
@@ -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
diff --git a/.gitea/workflows/pip-audit.yaml b/.gitea/workflows/pip-audit.yaml
new file mode 100644
index 0000000..b131169
--- /dev/null
+++ b/.gitea/workflows/pip-audit.yaml
@@ -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
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..36b13f1
--- /dev/null
+++ b/.gitignore
@@ -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
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..4b69624
--- /dev/null
+++ b/Dockerfile
@@ -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"]
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..164f6e0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# service-musicbrainz
+
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..112ee1a
--- /dev/null
+++ b/pyproject.toml
@@ -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",
+]
\ No newline at end of file
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..af3ee57
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1 @@
+ruff
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..4c8023e
--- /dev/null
+++ b/requirements.txt
@@ -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
\ No newline at end of file
diff --git a/scripts/filter_requirements.py b/scripts/filter_requirements.py
new file mode 100644
index 0000000..480db65
--- /dev/null
+++ b/scripts/filter_requirements.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""Write a copy of requirements.txt without the self-hosted packages.
+
+Usage: filter_requirements.py