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
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 %}
| ID | Store | Référence | Révision | Mis à jour |
{% for item in metrics.latest_products %}
| {{ item.id }} |
{{ item.source }} |
{{ item.reference }} |
{{ item.title[:40] }}{% if item.title|length > 40 %}…{% endif %} |
{{ item.updated }} |
{% endfor %}
{% 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)