chore: fix python style violations

Co-authored-by: OpenAI/GPT-5.6-Terra <vibecoder@antoineve.me>
Co-authored-by: OpenAI/GPT-5.6-Luna <vibecoder@antoineve.me>
Co-authored-by: MiniMax/MiniMax-M3 <vibecoder@antoineve.me>
Co-authored-by: DeepSeek/DeepSeek-v4-Flash <vibecoder@antoineve.me>
This commit is contained in:
2026-08-13 10:14:21 +02:00
parent 82beb6241f
commit c7a0a77d1f
14 changed files with 227 additions and 160 deletions

View File

@@ -1,8 +1,9 @@
import os
import sqlalchemy as sa
import tomllib
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import tomllib
import os
import sqlalchemy as sa
db = SQLAlchemy()
@@ -10,6 +11,7 @@ db = SQLAlchemy()
def _migrate_db(app):
"""Applique les migrations de schéma manquantes (pas d'Alembic)."""
import sqlite3
db_path = os.path.join(app.instance_path, "worklog.db")
if not os.path.exists(db_path):
return # Nouvelle DB, create_all() s'en charge
@@ -25,27 +27,40 @@ def _migrate_db(app):
engine = db.engine
if "motor_vehicle_id" not in columns:
with engine.connect() as conn:
conn.execute(sa.text(
"ALTER TABLE work_entries ADD COLUMN motor_vehicle_id VARCHAR(64)"
))
conn.execute(
sa.text("ALTER TABLE work_entries ADD COLUMN motor_vehicle_id VARCHAR(64)")
)
conn.commit()
_JOURS_FR = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"]
_MOIS_FR = ["", "janvier", "février", "mars", "avril", "mai", "juin",
"juillet", "août", "septembre", "octobre", "novembre", "décembre"]
_MOIS_FR = [
"",
"janvier",
"février",
"mars",
"avril",
"mai",
"juin",
"juillet",
"août",
"septembre",
"octobre",
"novembre",
"décembre",
]
_DAY_TYPE_LABELS = {
"WORK": "Travail",
"TT": "Télétravail",
"GARDE": "Garde",
"ASTREINTE": "Astreinte",
"FORMATION": "Formation",
"RTT": "RTT",
"CONGE": "Congé",
"MALADE": "Maladie",
"FERIE": "Férié",
"WORK": "Travail",
"TT": "Télétravail",
"GARDE": "Garde",
"ASTREINTE": "Astreinte",
"FORMATION": "Formation",
"RTT": "RTT",
"CONGE": "Congé",
"MALADE": "Maladie",
"FERIE": "Férié",
}
@@ -55,7 +70,6 @@ def _day_type_fr(code):
def _date_fr(d):
"""Formate une date en français : 'mercredi 11 mars 2026'."""
from datetime import date as date_type
jour = _JOURS_FR[d.weekday()]
mois = _MOIS_FR[d.month]
return f"{jour} {d.day} {mois} {d.year}"
@@ -66,7 +80,9 @@ def create_app(config_path=None):
os.makedirs(app.instance_path, exist_ok=True)
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{os.path.join(app.instance_path, 'worklog.db')}"
app.config["SQLALCHEMY_DATABASE_URI"] = (
f"sqlite:///{os.path.join(app.instance_path, 'worklog.db')}"
)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-secret-change-in-prod")
@@ -86,6 +102,7 @@ def create_app(config_path=None):
from app.routes.dashboard import bp as dashboard_bp
from app.routes.entries import bp as entries_bp
from app.routes.reports import bp as reports_bp
app.register_blueprint(dashboard_bp)
app.register_blueprint(entries_bp)
app.register_blueprint(reports_bp)

View File

@@ -1,34 +1,40 @@
from app import db
from app.models import WorkEntry, LeaveBalance
import sqlalchemy as sa
from datetime import date
import sqlalchemy as sa
from app import db
from app.models import LeaveBalance, WorkEntry
def compute_leave_used(year: int) -> dict[str, int]:
start = date(year, 1, 1)
end = date(year, 12, 31)
conges = db.session.scalar(
sa.select(sa.func.count()).where(
WorkEntry.date.between(start, end),
WorkEntry.day_type == "CONGE",
conges = (
db.session.scalar(
sa.select(sa.func.count()).where(
WorkEntry.date.between(start, end),
WorkEntry.day_type == "CONGE",
)
)
) or 0
or 0
)
rtt = db.session.scalar(
sa.select(sa.func.count()).where(
WorkEntry.date.between(start, end),
WorkEntry.day_type == "RTT",
rtt = (
db.session.scalar(
sa.select(sa.func.count()).where(
WorkEntry.date.between(start, end),
WorkEntry.day_type == "RTT",
)
)
) or 0
or 0
)
return {"conges": conges, "rtt": rtt}
def get_or_create_balance(year: int) -> LeaveBalance:
balance = db.session.scalar(
sa.select(LeaveBalance).where(LeaveBalance.year == year)
)
balance = db.session.scalar(sa.select(LeaveBalance).where(LeaveBalance.year == year))
if balance is None:
balance = LeaveBalance(year=year)
db.session.add(balance)

View File

@@ -32,7 +32,9 @@ def compute_co2_grams(km_by_vehicle: dict[str, int], vehicles: dict) -> float:
return total
def compute_frais_reels(total_km_moteur: float, tranches: list[dict], electric: bool = False) -> float:
def compute_frais_reels(
total_km_moteur: float, tranches: list[dict], electric: bool = False
) -> float:
"""
Calcule les frais réels fiscaux selon le barème kilométrique.
km_max = 0 signifie "pas de limite" (dernière tranche).

View File

@@ -1,8 +1,10 @@
from app import db
from datetime import date, time, datetime
from datetime import date, datetime, time
import sqlalchemy as sa
import sqlalchemy.orm as so
from app import db
class WorkEntry(db.Model):
__tablename__ = "work_entries"

View File

@@ -1,12 +1,14 @@
from flask import Blueprint, render_template
from datetime import date, timedelta
import sqlalchemy as sa
from flask import Blueprint, render_template
from app import db
from app.models import WorkEntry
from app.business.time_calc import minutes_to_str, work_minutes_reference
from app.business.travel_calc import compute_km_for_entry, compute_co2_grams
from app.business.leave_calc import compute_leave_used, get_or_create_balance
from app.config_loader import get_vehicles, get_journeys
from app.business.time_calc import minutes_to_str, work_minutes_reference
from app.business.travel_calc import compute_co2_grams, compute_km_for_entry
from app.config_loader import get_journeys, get_vehicles
from app.models import WorkEntry
bp = Blueprint("dashboard", __name__)
@@ -45,9 +47,7 @@ def index():
balance = get_or_create_balance(year)
used = compute_leave_used(year)
today_entry = db.session.scalar(
sa.select(WorkEntry).where(WorkEntry.date == today)
)
today_entry = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.date == today))
return render_template(
"dashboard.html",

View File

@@ -1,9 +1,16 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from datetime import date, time
import sqlalchemy as sa
from flask import Blueprint, flash, redirect, render_template, request, url_for
from app import db
from app.models import WorkEntry, TimeSlot
from app.config_loader import get_journeys, get_motor_vehicles, day_types_without_journey, journey_has_motor
from app.config_loader import (
day_types_without_journey,
get_journeys,
get_motor_vehicles,
journey_has_motor,
)
from app.models import TimeSlot, WorkEntry
bp = Blueprint("entries", __name__, url_prefix="/entries")
@@ -22,9 +29,7 @@ DAY_TYPES = [
@bp.route("/")
def list_entries():
entries = db.session.scalars(
sa.select(WorkEntry).order_by(WorkEntry.date.desc())
).all()
entries = db.session.scalars(sa.select(WorkEntry).order_by(WorkEntry.date.desc())).all()
return render_template("entry_list.html", entries=entries)
@@ -51,9 +56,7 @@ def entry_form(entry_id=None):
journey_profile_id = None
if entry is None:
existing = db.session.scalar(
sa.select(WorkEntry).where(WorkEntry.date == entry_date)
)
existing = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.date == entry_date))
if existing:
flash(f"Une entrée existe déjà pour le {entry_date}.", "error")
return redirect(url_for("entries.entry_form"))
@@ -72,11 +75,13 @@ def entry_form(entry_id=None):
ends = request.form.getlist("end_time")
for s, e in zip(starts, ends):
if s and e:
db.session.add(TimeSlot(
entry=entry,
start_time=time.fromisoformat(s),
end_time=time.fromisoformat(e),
))
db.session.add(
TimeSlot(
entry=entry,
start_time=time.fromisoformat(s),
end_time=time.fromisoformat(e),
)
)
db.session.commit()
flash("Entrée enregistrée.", "success")

View File

@@ -1,19 +1,30 @@
from flask import Blueprint, render_template, request
from datetime import date
from collections import defaultdict
from datetime import date
import sqlalchemy as sa
from flask import Blueprint, render_template, request
from app import db
from app.business.time_calc import count_day_types, minutes_to_str, monthly_stats
from app.business.travel_calc import compute_co2_grams, compute_frais_reels, compute_km_for_entry
from app.config_loader import get_bareme, get_journeys, get_vehicles
from app.models import WorkEntry
from app.business.travel_calc import compute_km_for_entry, compute_co2_grams, compute_frais_reels
from app.business.time_calc import count_day_types, monthly_stats, minutes_to_str
from app.config_loader import get_vehicles, get_journeys, get_bareme
bp = Blueprint("reports", __name__, url_prefix="/reports")
MONTHS_FR = {
1: "Janvier", 2: "Février", 3: "Mars", 4: "Avril",
5: "Mai", 6: "Juin", 7: "Juillet", 8: "Août",
9: "Septembre", 10: "Octobre", 11: "Novembre", 12: "Décembre",
1: "Janvier",
2: "Février",
3: "Mars",
4: "Avril",
5: "Mai",
6: "Juin",
7: "Juillet",
8: "Août",
9: "Septembre",
10: "Octobre",
11: "Novembre",
12: "Décembre",
}
@@ -73,7 +84,9 @@ def index():
"km_by_vehicle": month_km,
"km_total": sum(month_km.values()),
"median_daily_str": minutes_to_str(stats["median_daily_min"]) if month_entries else "",
"median_weekly_str": minutes_to_str(stats["median_weekly_min"]) if month_entries else "",
"median_weekly_str": minutes_to_str(stats["median_weekly_min"])
if month_entries
else "",
}
return render_template(

View File

@@ -27,29 +27,26 @@ import sqlalchemy as sa
sys.path.insert(0, str(Path(__file__).parent.parent))
from app import create_app, db
from app.models import WorkEntry, TimeSlot
from app.config_loader import day_types_without_journey, journey_has_motor
from app.models import TimeSlot, WorkEntry
DAY_TYPES = {
"WORK", "TT", "GARDE", "ASTREINTE", "FORMATION", "RTT", "CONGE", "MALADE", "FERIE"
}
DAY_TYPES = {"WORK", "TT", "GARDE", "ASTREINTE", "FORMATION", "RTT", "CONGE", "MALADE", "FERIE"}
def main(csv_path: str, config_path: str = None):
"""Importe les données depuis un fichier CSV vers la base de données."""
# Créer l'application Flask avec la config
app = create_app(config_path=config_path)
with app.app_context():
conflicts = []
imported_count = 0
# Lire le fichier CSV
with open(csv_path, "r", encoding="utf-8") as f:
csv_reader = csv.DictReader(f)
for row_num, row in enumerate(csv_reader, start=2):
try:
# Parser la date
@@ -57,38 +54,38 @@ def main(csv_path: str, config_path: str = None):
except (ValueError, AttributeError):
conflicts.append(f"Ligne {row_num}: date invalide ou manquante")
continue
# Valider le type de jour
day_type = row.get("day_type", "WORK").strip().upper()
if day_type not in DAY_TYPES:
conflicts.append(f"Ligne {row_num}: type de jour invalide '{day_type}'")
continue
# Récupérer les autres champs
journey_profile_id = row.get("journey_profile_id", "").strip() or None
motor_vehicle_id = row.get("motor_vehicle_id", "").strip() or None
comment = row.get("comment", "").strip() or None
# Si le type de jour n'a pas de trajet, forcer journey_profile_id à None
if day_type in day_types_without_journey():
journey_profile_id = None
# Si le profil de trajet n'a pas de moteur, forcer motor_vehicle_id à None
if journey_profile_id and not journey_has_motor(journey_profile_id):
motor_vehicle_id = None
# Vérifier si une entrée existe déjà pour cette date
existing = db.session.scalar(
sa.select(WorkEntry).where(WorkEntry.date == entry_date)
)
if existing:
# Vérifier si les données sont différentes
is_different = (
existing.day_type != day_type or
existing.journey_profile_id != journey_profile_id or
existing.motor_vehicle_id != motor_vehicle_id or
existing.comment != comment
existing.day_type != day_type
or existing.journey_profile_id != journey_profile_id
or existing.motor_vehicle_id != motor_vehicle_id
or existing.comment != comment
)
if is_different:
conflicts.append(
@@ -96,7 +93,7 @@ def main(csv_path: str, config_path: str = None):
f"Données existantes conservées."
)
continue
# Créer la nouvelle entrée
entry = WorkEntry(
date=entry_date,
@@ -106,58 +103,64 @@ def main(csv_path: str, config_path: str = None):
comment=comment,
)
db.session.add(entry)
# Ajouter les plages horaires
start_times = row.get("start_time", "").split(";")
end_times = row.get("end_time", "").split(";")
for s, e in zip(start_times, end_times):
s = s.strip()
e = e.strip()
if s and e:
try:
db.session.add(TimeSlot(
entry=entry,
start_time=time.fromisoformat(s),
end_time=time.fromisoformat(e),
))
db.session.add(
TimeSlot(
entry=entry,
start_time=time.fromisoformat(s),
end_time=time.fromisoformat(e),
)
)
except (ValueError, AttributeError):
conflicts.append(f"Ligne {row_num}: format d'heure invalide '{s}' ou '{e}'")
conflicts.append(
f"Ligne {row_num}: format d'heure invalide '{s}' ou '{e}'"
)
db.session.rollback()
break
else:
imported_count += 1
continue
db.session.rollback()
break
# Valider et commiter
try:
db.session.commit()
except Exception as e:
except sa.exc.SQLAlchemyError as e:
db.session.rollback()
print(f"Erreur lors du commit: {e}", file=sys.stderr)
return 1
# Afficher les résultats
print(f"Import terminé: {imported_count} entrée(s) importée(s)")
if conflicts:
print(f"\n⚠️ {len(conflicts)} avertissement(s):")
for conflict in conflicts:
print(f" - {conflict}")
return 0
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Importer un fichier CSV dans la base de données")
parser.add_argument("csv_file", help="Chemin vers le fichier CSV à importer")
parser.add_argument("--config", default=None, help="Chemin vers le fichier config.toml (optionnel)")
parser.add_argument(
"--config", default=None, help="Chemin vers le fichier config.toml (optionnel)"
)
args = parser.parse_args()
sys.exit(main(args.csv_file, args.config))

View File

@@ -1,11 +1,14 @@
import pytest
from app import create_app, db as _db
from app import create_app
from app import db as _db
@pytest.fixture
def app(tmp_path):
config_path = tmp_path / "config.toml"
config_path.write_text("""
config_path.write_text(
"""
[vehicles.citadine]
name = "Citadine électrique"
fuel = "electric"
@@ -59,7 +62,9 @@ forfait = 699
km_max = 0
taux = 0.364
forfait = 0
""", encoding="utf-8")
""",
encoding="utf-8",
)
application = create_app(config_path=str(config_path))
application.config["TESTING"] = True

View File

@@ -1,6 +1,7 @@
def test_get_vehicles_returns_configured_vehicles(app):
with app.app_context():
from app.config_loader import get_vehicles
vehicles = get_vehicles()
assert "familiale" in vehicles
assert vehicles["familiale"]["co2_per_km"] == 142
@@ -9,6 +10,7 @@ def test_get_vehicles_returns_configured_vehicles(app):
def test_get_motor_vehicles_excludes_velo(app):
with app.app_context():
from app.config_loader import get_motor_vehicles
motor = get_motor_vehicles()
assert "familiale" in motor
assert "citadine" in motor
@@ -19,6 +21,7 @@ def test_get_motor_vehicles_excludes_velo(app):
def test_get_journeys_returns_profiles(app):
with app.app_context():
from app.config_loader import get_journeys
journeys = get_journeys()
assert "moteur_seul" in journeys
assert journeys["moteur_seul"]["distances"]["moteur"] == 25
@@ -27,6 +30,7 @@ def test_get_journeys_returns_profiles(app):
def test_journey_has_motor_true(app):
with app.app_context():
from app.config_loader import journey_has_motor
assert journey_has_motor("moteur_seul") is True
assert journey_has_motor("moteur_velo") is True
@@ -34,6 +38,7 @@ def test_journey_has_motor_true(app):
def test_journey_has_motor_false(app):
with app.app_context():
from app.config_loader import journey_has_motor
assert journey_has_motor("velo_seul") is False
assert journey_has_motor(None) is False
@@ -41,6 +46,7 @@ def test_journey_has_motor_false(app):
def test_get_bareme_returns_tranches(app):
with app.app_context():
from app.config_loader import get_bareme
tranches = get_bareme(2025, 5)
assert len(tranches) == 3
assert tranches[0]["taux"] == 0.548
@@ -49,6 +55,7 @@ def test_get_bareme_returns_tranches(app):
def test_day_types_without_journey(app):
with app.app_context():
from app.config_loader import day_types_without_journey
types = day_types_without_journey()
assert "TT" in types
assert "WORK" not in types

View File

@@ -1,8 +1,8 @@
from app.business.leave_calc import compute_leave_used, get_or_create_balance
from app.models import WorkEntry, LeaveBalance
from app import db
from datetime import date
import sqlalchemy as sa
from app import db
from app.business.leave_calc import compute_leave_used, get_or_create_balance
from app.models import LeaveBalance, WorkEntry
def test_compute_leave_used_conges(app):

View File

@@ -1,8 +1,10 @@
from app.models import WorkEntry, TimeSlot
from app import db
from datetime import date, time
from datetime import date
import sqlalchemy as sa
from app import db
from app.models import WorkEntry
def test_dashboard_empty(client):
response = client.get("/")
@@ -17,21 +19,23 @@ def test_entry_form_get(client):
def test_create_entry(client, app):
response = client.post("/entries/new", data={
"date": "2025-06-02",
"day_type": "WORK",
"journey_profile_id": "moteur_seul",
"motor_vehicle_id": "familiale",
"start_time": ["09:00"],
"end_time": ["17:45"],
"comment": "",
}, follow_redirects=True)
response = client.post(
"/entries/new",
data={
"date": "2025-06-02",
"day_type": "WORK",
"journey_profile_id": "moteur_seul",
"motor_vehicle_id": "familiale",
"start_time": ["09:00"],
"end_time": ["17:45"],
"comment": "",
},
follow_redirects=True,
)
assert response.status_code == 200
with app.app_context():
entry = db.session.scalar(
sa.select(WorkEntry).where(WorkEntry.date == date(2025, 6, 2))
)
entry = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.date == date(2025, 6, 2)))
assert entry is not None
assert entry.day_type == "WORK"
assert len(entry.time_slots) == 1
@@ -66,29 +70,29 @@ def test_delete_entry(client, app):
assert response.status_code == 200
with app.app_context():
deleted = db.session.scalar(
sa.select(WorkEntry).where(WorkEntry.id == entry_id)
)
deleted = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.id == entry_id))
assert deleted is None
def test_create_entry_velo_no_motor_vehicle(client, app):
"""Un trajet vélo seul ne doit pas enregistrer de motor_vehicle_id."""
response = client.post("/entries/new", data={
"date": "2025-06-10",
"day_type": "WORK",
"journey_profile_id": "velo_seul",
"motor_vehicle_id": "",
"start_time": ["08:30"],
"end_time": ["17:00"],
"comment": "",
}, follow_redirects=True)
response = client.post(
"/entries/new",
data={
"date": "2025-06-10",
"day_type": "WORK",
"journey_profile_id": "velo_seul",
"motor_vehicle_id": "",
"start_time": ["08:30"],
"end_time": ["17:00"],
"comment": "",
},
follow_redirects=True,
)
assert response.status_code == 200
with app.app_context():
entry = db.session.scalar(
sa.select(WorkEntry).where(WorkEntry.date == date(2025, 6, 10))
)
entry = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.date == date(2025, 6, 10)))
assert entry is not None
assert entry.motor_vehicle_id is None

View File

@@ -1,7 +1,7 @@
from app.business.time_calc import (
minutes_to_str,
work_minutes_reference,
week_balance_minutes,
work_minutes_reference,
)
@@ -42,10 +42,11 @@ def test_week_balance_negative():
assert week_balance_minutes(2200, 2325) == -125
from app.business.time_calc import count_day_types
from app.models import WorkEntry, TimeSlot
from app.business.time_calc import monthly_stats
from datetime import date, time as dtime
from datetime import date
from datetime import time as dtime
from app.business.time_calc import count_day_types, monthly_stats
from app.models import TimeSlot, WorkEntry
def test_count_day_types_basic():
@@ -67,8 +68,10 @@ def _entry(d: date, *slots: tuple[str, str]) -> WorkEntry:
"""Helper : crée un WorkEntry avec des TimeSlots."""
entry = WorkEntry(date=d, day_type="WORK")
entry.time_slots = [
TimeSlot(start_time=dtime(*[int(x) for x in s.split(":")]),
end_time=dtime(*[int(x) for x in e.split(":")]))
TimeSlot(
start_time=dtime(*[int(x) for x in s.split(":")]),
end_time=dtime(*[int(x) for x in e.split(":")]),
)
for s, e in slots
]
return entry
@@ -89,9 +92,9 @@ def test_monthly_stats_empty():
def test_monthly_stats_median_daily_odd():
# 420, 465, 510 → médiane = 465
entries = [
_entry(date(2025, 1, 6), ("9:00", "16:00")), # 420 min
_entry(date(2025, 1, 7), ("9:00", "16:45")), # 465 min
_entry(date(2025, 1, 8), ("9:00", "17:30")), # 510 min
_entry(date(2025, 1, 6), ("9:00", "16:00")), # 420 min
_entry(date(2025, 1, 7), ("9:00", "16:45")), # 465 min
_entry(date(2025, 1, 8), ("9:00", "17:30")), # 510 min
]
result = monthly_stats(entries)
assert result["median_daily_min"] == 465
@@ -114,7 +117,7 @@ def test_monthly_stats_median_weekly():
entries = [
_entry(date(2025, 1, 6), ("9:00", "16:45")), # sem 2
_entry(date(2025, 1, 7), ("9:00", "16:45")), # sem 2
_entry(date(2025, 1, 13), ("9:00", "16:00")), # sem 3
_entry(date(2025, 1, 13), ("9:00", "16:00")), # sem 3
]
result = monthly_stats(entries)
assert result["median_weekly_min"] == 675

View File

@@ -1,7 +1,7 @@
from app.business.travel_calc import (
compute_km_for_entry,
compute_co2_grams,
compute_frais_reels,
compute_km_for_entry,
)
VEHICLES = {
@@ -17,15 +17,15 @@ JOURNEYS = {
}
TRANCHES_CV3 = [
{"km_max": 5000, "taux": 0.529, "forfait": 0},
{"km_max": 5000, "taux": 0.529, "forfait": 0},
{"km_max": 20000, "taux": 0.316, "forfait": 1065},
{"km_max": 0, "taux": 0.370, "forfait": 0},
{"km_max": 0, "taux": 0.370, "forfait": 0},
]
TRANCHES_CV5 = [
{"km_max": 5000, "taux": 0.636, "forfait": 0},
{"km_max": 5000, "taux": 0.636, "forfait": 0},
{"km_max": 20000, "taux": 0.357, "forfait": 1395},
{"km_max": 0, "taux": 0.427, "forfait": 0},
{"km_max": 0, "taux": 0.427, "forfait": 0},
]