import os from typing import Any, Dict, List, Optional, Tuple from decimal import Decimal from psycopg2.extras import RealDictCursor import psycopg2 import redis from flask import Flask, jsonify, render_template_string app = Flask(__name__) def _env_int(name: str, default: int) -> int: try: return int(os.getenv(name, "") or default) except ValueError: return default def get_db_connection(): return psycopg2.connect( host=os.getenv("PW_DB_HOST", "postgres"), port=_env_int("PW_DB_PORT", 5432), dbname=os.getenv("PW_DB_NAME", "pricewatch"), user=os.getenv("PW_DB_USER", "pricewatch"), password=os.getenv("PW_DB_PASSWORD", "pricewatch"), ) def fetch_db_metrics() -> Tuple[Dict[str, Any], Optional[str]]: data: Dict[str, Any] = {"counts": {}, "latest_products": []} try: with get_db_connection() as conn: with conn.cursor() as cur: cur.execute("SELECT COUNT(*) FROM products") data["counts"]["products"] = cur.fetchone()[0] cur.execute("SELECT COUNT(*) FROM price_history") data["counts"]["price_history"] = cur.fetchone()[0] cur.execute( "SELECT COUNT(*) FROM scraping_logs" ) data["counts"]["scraping_logs"] = cur.fetchone()[0] cur.execute( """ SELECT id, source, reference, title, last_updated_at FROM products ORDER BY last_updated_at DESC LIMIT 5 """ ) rows = cur.fetchall() data["latest_products"] = [ { "id": row[0], "source": row[1], "reference": row[2], "title": row[3] or "Sans titre", "updated": row[4].strftime("%Y-%m-%d %H:%M:%S") if row[4] else "n/a", } for row in rows ] return data, None except Exception as exc: # pragma: no cover (simple explorer) return data, str(exc) def _serialize_decimal(value): if isinstance(value, Decimal): return float(value) return value def fetch_products_list(limit: int = 200) -> Tuple[List[Dict[str, Any]], Optional[str]]: rows: List[Dict[str, Any]] = [] try: with get_db_connection() as conn: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( """ SELECT p.id, p.source, p.reference, p.title, p.url, p.category, p.description, p.currency, p.msrp, p.last_updated_at, ph.price, ph.stock_status, ph.fetch_status, ph.fetch_method, ph.fetched_at FROM products p LEFT JOIN LATERAL ( SELECT price, stock_status, fetch_status, fetch_method, fetched_at FROM price_history WHERE product_id = p.id ORDER BY fetched_at DESC LIMIT 1 ) ph ON true ORDER BY p.last_updated_at DESC LIMIT %s """, (limit,), ) fetched = cur.fetchall() for item in fetched: serialized = {key: _serialize_decimal(value) for key, value in item.items()} if serialized.get("last_updated_at"): serialized["last_updated_at"] = serialized["last_updated_at"].strftime( "%Y-%m-%d %H:%M:%S" ) if serialized.get("fetched_at"): serialized["fetched_at"] = serialized["fetched_at"].strftime( "%Y-%m-%d %H:%M:%S" ) rows.append(serialized) return rows, None except Exception as exc: return rows, str(exc) def get_redis_client() -> redis.Redis: return redis.Redis( host=os.getenv("PW_REDIS_HOST", "redis"), port=_env_int("PW_REDIS_PORT", 6379), db=_env_int("PW_REDIS_DB", 0), socket_connect_timeout=2, socket_timeout=2, ) def check_redis() -> Tuple[str, Optional[str]]: client = get_redis_client() try: client.ping() return "OK", None except Exception as exc: return "KO", str(exc) TEMPLATE = """ PriceWatch Analytics UI

PriceWatch Analytics UI

PostgreSQL : {{ db_status }} · Redis : {{ redis_status }}

Vue rapide

Base : {{ db_status }}
Redis : {{ redis_status }}
{% if db_error or redis_error %}

Erreurs : {{ db_error or '' }} {{ redis_error or '' }}

{% endif %}

Stats métier

Produits{{ metrics.counts.products }}
Historique prix{{ metrics.counts.price_history }}
Logs de scraping{{ metrics.counts.scraping_logs }}

Produits récemment mis à jour

{% if metrics.latest_products %} {% for item in metrics.latest_products %} {% endfor %}
IDStoreRéférenceRévisionMis à jour
{{ item.id }} {{ item.source }} {{ item.reference }} {{ item.title[:40] }}{% if item.title|length > 40 %}…{% endif %} {{ item.updated }}
{% else %}

Aucun produit enregistré.

{% endif %}

Parcourir la base (produits)

0 / 0
Titre
-
Store
-
Référence
-
Dernier prix
-
Devise
-
Prix conseillé
-
Stock
-
Catégorie
-
Description
-
Dernière mise à jour
-
Historique dernier scrap
-
""" @app.route("/") def root(): metrics, db_error = fetch_db_metrics() redis_status, redis_error = check_redis() return render_template_string( TEMPLATE, metrics=metrics, db_status="connecté" if db_error is None else "erreur", db_error=db_error, redis_status=redis_status, redis_error=redis_error, ) @app.route("/products.json") def products_json(): products, error = fetch_products_list() if error: return jsonify({"error": error}), 500 return jsonify(products) if __name__ == "__main__": app.run(host="0.0.0.0", port=80)