This commit is contained in:
chase
2026-05-04 11:07:31 +03:00
commit 52280c721f
44 changed files with 991 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
__pycache__
*.pyc
.env
.git
*.md
+34
View File
@@ -0,0 +1,34 @@
# syntax=docker/dockerfile:1
FROM python:3.12-slim-bookworm
LABEL service="api" description="Flask API + Postgres + upload volume"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PIP_NO_CACHE_DIR=1
WORKDIR /app
# Системные зависимости для psycopg2-binary и healthcheck
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
wget \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --upgrade pip && pip install -r requirements.txt
# Копируем код приложения в образ
COPY app.py .
# Не root в runtime (uid в образе python обычно есть)
RUN useradd --create-home --uid 10001 appuser \
&& mkdir -p /data/uploads \
&& chown -R appuser:appuser /data /app
USER appuser
EXPOSE 5000
# Продакшен-сервер (не встроенный dev-сервер Flask)
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app:app"]
+115
View File
@@ -0,0 +1,115 @@
"""
Минимальное API для демонстрации Compose:
- подключение к Postgres по DATABASE_URL
- таблица notes при старте
- загрузка файла в каталог UPLOAD_DIR (том)
"""
import os
import uuid
from pathlib import Path
from flask import Flask, jsonify, request
import psycopg2
from psycopg2.extras import RealDictCursor
app = Flask(__name__)
UPLOAD_DIR = Path(os.environ.get("UPLOAD_DIR", "/data/uploads"))
DATABASE_URL = os.environ.get(
"DATABASE_URL",
"postgresql://app:app@localhost:5432/appdb",
)
def get_conn():
return psycopg2.connect(DATABASE_URL, cursor_factory=RealDictCursor)
def init_db():
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
with get_conn() as conn:
with conn.cursor() as cur:
cur.execute(
"""
CREATE TABLE IF NOT EXISTS notes (
id SERIAL PRIMARY KEY,
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
"""
)
conn.commit()
@app.route("/health")
def health():
try:
with get_conn() as conn:
with conn.cursor() as cur:
cur.execute("SELECT 1 AS ok;")
row = cur.fetchone()
return jsonify({"status": "ok", "db": bool(row)}), 200
except Exception as exc: # noqa: BLE001 — демо
return jsonify({"status": "error", "detail": str(exc)}), 503
def _note_row(r):
d = dict(r)
if d.get("created_at") is not None:
d["created_at"] = d["created_at"].isoformat()
return d
@app.route("/notes", methods=["GET"])
def list_notes():
with get_conn() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT id, body, created_at FROM notes ORDER BY id DESC LIMIT 50;"
)
rows = cur.fetchall()
return jsonify([_note_row(r) for r in rows])
@app.route("/notes", methods=["POST"])
def create_note():
data = request.get_json(silent=True) or {}
body = (data.get("body") or "").strip()
if not body:
return jsonify({"error": "body required"}), 400
with get_conn() as conn:
with conn.cursor() as cur:
cur.execute(
"INSERT INTO notes (body) VALUES (%s) RETURNING id, body, created_at;",
(body,),
)
row = cur.fetchone()
conn.commit()
return jsonify(_note_row(row)), 201
@app.route("/upload", methods=["POST"])
def upload():
if "file" not in request.files:
return jsonify({"error": "file field required"}), 400
f = request.files["file"]
if not f.filename:
return jsonify({"error": "empty filename"}), 400
safe_name = f"{uuid.uuid4().hex}_{f.filename}"
dest = UPLOAD_DIR / safe_name
f.save(dest)
return jsonify({"saved_as": safe_name, "path_in_container": str(dest)}), 201
@app.route("/uploads", methods=["GET"])
def list_uploads():
files = sorted(p.name for p in UPLOAD_DIR.iterdir() if p.is_file())
return jsonify({"files": files})
with app.app_context():
init_db()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
@@ -0,0 +1,3 @@
flask==3.0.3
gunicorn==22.0.0
psycopg2-binary==2.9.9