push
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
.env
|
||||
.git
|
||||
*.md
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user