chore: ajoute ruff (lint) et corrige les violations

- Config ruff dans pyproject.toml : règles E/W/F/I/UP/B, whitelist des appels
  d'injection FastAPI (Depends/File/Form/Query) pour B008, B904 ignoré
  (traduction volontaire des exceptions en réponses HTTP)
- Auto-fixes : tri des imports, suppression d'imports inutilisés, annotations
  PEP 604, f-strings sans placeholder, modes open redondants
- Suppression de variables inutilisées (config.reset_setting, parser locataires)

ruff check . : All checks passed ; 61 tests OK

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 10:20:36 +02:00
parent 4606d6785b
commit d42bfecab4
34 changed files with 148 additions and 105 deletions

View File

@@ -18,6 +18,7 @@ plesna-gerance = "plesna_gerance.cli:main"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"pytest>=8.0", "pytest>=8.0",
"ruff>=0.6",
] ]
[tool.pytest.ini_options] [tool.pytest.ini_options]
@@ -25,6 +26,28 @@ testpaths = ["tests"]
pythonpath = ["src"] pythonpath = ["src"]
addopts = "-q" addopts = "-q"
[tool.ruff]
target-version = "py310"
line-length = 88
src = ["src", "tests"]
[tool.ruff.lint]
# E/W: pycodestyle, F: pyflakes, I: isort, UP: pyupgrade, B: bugbear
select = ["E", "W", "F", "I", "UP", "B"]
ignore = [
"E501", # longueur de ligne gérée par le formateur, pas bloquante
"B904", # raise sans `from` : on traduit volontairement les exceptions en HTTP
]
[tool.ruff.lint.flake8-bugbear]
# Appels idiomatiques FastAPI en argument par défaut (injection de dépendances)
extend-immutable-calls = [
"fastapi.Depends",
"fastapi.File",
"fastapi.Form",
"fastapi.Query",
]
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"

View File

@@ -10,16 +10,17 @@ from fastapi.staticfiles import StaticFiles
from .. import __version__ from .. import __version__
from ..database import init_db from ..database import init_db
from .routes import ( from .routes import (
extraction_router,
documents_router,
tags_router,
analytics_router, analytics_router,
dashboard_router,
revenus_router,
ia_router,
config_router, config_router,
dashboard_router,
documents_router,
extraction_router,
ia_router,
revenus_router,
tags_router,
) )
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""Initialize database on application startup.""" """Initialize database on application startup."""

View File

@@ -1,13 +1,13 @@
"""API Routes package.""" """API Routes package."""
from .extraction import router as extraction_router
from .documents import router as documents_router
from .tags import router as tags_router
from .analytics import router as analytics_router from .analytics import router as analytics_router
from .dashboard import router as dashboard_router
from .revenus import router as revenus_router
from .ia import router as ia_router
from .config import router as config_router from .config import router as config_router
from .dashboard import router as dashboard_router
from .documents import router as documents_router
from .extraction import router as extraction_router
from .ia import router as ia_router
from .revenus import router as revenus_router
from .tags import router as tags_router
__all__ = [ __all__ = [
"extraction_router", "extraction_router",

View File

@@ -1,25 +1,25 @@
"""Analytics routes - Data analysis and reporting endpoints.""" """Analytics routes - Data analysis and reporting endpoints."""
from datetime import date
from collections import defaultdict from collections import defaultdict
from datetime import date
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from sqlalchemy import distinct, func, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import select, func, distinct, extract
from ...database import get_session from ...database import get_session
from ...database.models import Depense, Document, Immeuble, Lot, Tag from ...database.models import Depense, Document, Immeuble, Lot, Tag
from ..schemas.models import ( from ..schemas.models import (
ImmeubleResponse, CategorySummary,
LotResponse,
DepenseDetail, DepenseDetail,
DepensesSummary, DepensesSummary,
CategorySummary,
TagSummary,
MonthlySummary,
FournisseurSummary,
FournisseurResponse, FournisseurResponse,
FournisseurSummary,
ImmeubleResponse,
LotResponse,
MonthlySummary,
TagResponse, TagResponse,
TagSummary,
) )
router = APIRouter(prefix="/api", tags=["analytics"]) router = APIRouter(prefix="/api", tags=["analytics"])

View File

@@ -4,13 +4,13 @@ from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ...database import get_session, DatabaseService from ...database import DatabaseService, get_session
from ...database.models import Tag from ...database.models import Tag
from ...services.settings_service import ( from ...services.settings_service import (
SETTINGS_REGISTRY,
delete_setting,
get_all_settings, get_all_settings,
set_setting, set_setting,
delete_setting,
SETTINGS_REGISTRY,
) )
router = APIRouter(prefix="/api/config", tags=["config"]) router = APIRouter(prefix="/api/config", tags=["config"])
@@ -67,7 +67,7 @@ async def reset_setting(
"""Reset un setting au défaut (supprime l'override DB).""" """Reset un setting au défaut (supprime l'override DB)."""
if key not in SETTINGS_REGISTRY: if key not in SETTINGS_REGISTRY:
raise HTTPException(status_code=404, detail=f"Setting inconnu : {key}") raise HTTPException(status_code=404, detail=f"Setting inconnu : {key}")
deleted = delete_setting(session, key) delete_setting(session, key)
# Return the resolved value after deletion # Return the resolved value after deletion
all_settings = get_all_settings(session) all_settings = get_all_settings(session)
return all_settings[key] return all_settings[key]

View File

@@ -1,21 +1,21 @@
"""Dashboard routes - Aggregated data for the home page.""" """Dashboard routes - Aggregated data for the home page."""
from datetime import date, timedelta
from collections import defaultdict from collections import defaultdict
from datetime import date, timedelta
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from sqlalchemy import select, func, desc
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import desc, func, select
from sqlalchemy.orm import Session
from ...database import get_session from ...database import get_session
from ...database.models import ( from ...database.models import (
Depense,
Document, Document,
Immeuble, Immeuble,
Lot,
Locataire, Locataire,
Lot,
Revenu, Revenu,
Depense,
) )
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"]) router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])

View File

@@ -4,17 +4,17 @@ import json
import logging import logging
from datetime import datetime from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, File, UploadFile, Form from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from fastapi.responses import Response, JSONResponse from fastapi.responses import JSONResponse, Response
from sqlalchemy.orm import Session
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.orm import Session
from ...database import get_session, DatabaseService, storage from ...database import DatabaseService, get_session, storage
from ...database.models import Document, Immeuble, Lot, Locataire, Revenu, Depense from ...database.models import Depense, Document, Immeuble, Locataire, Lot, Revenu
from ...database.service import DuplicateDocumentError from ...database.service import DuplicateDocumentError
from ...extractor import extract_compte_rendu from ...extractor import extract_compte_rendu
from ...utils.uploads import read_upload_limited, UploadTooLargeError from ...utils.uploads import UploadTooLargeError, read_upload_limited
from ..schemas import SaveRequest, SaveResponse, DocumentSummary from ..schemas import DocumentSummary, SaveRequest, SaveResponse
router = APIRouter(prefix="/api", tags=["documents"]) router = APIRouter(prefix="/api", tags=["documents"])

View File

@@ -7,7 +7,7 @@ from fastapi import APIRouter, File, HTTPException, UploadFile
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from ...extractor import extract_compte_rendu from ...extractor import extract_compte_rendu
from ...utils.uploads import read_upload_limited, UploadTooLargeError from ...utils.uploads import UploadTooLargeError, read_upload_limited
router = APIRouter(prefix="/api", tags=["extraction"]) router = APIRouter(prefix="/api", tags=["extraction"])

View File

@@ -2,11 +2,10 @@
import logging import logging
import httpx
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
import httpx
from ...services.ollama_service import ask_ollama, check_ollama_health from ...services.ollama_service import ask_ollama, check_ollama_health
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -1,19 +1,18 @@
"""Revenus routes - Dedicated endpoints for rental income analytics.""" """Revenus routes - Dedicated endpoints for rental income analytics."""
from datetime import date, timedelta from datetime import date, timedelta
from collections import defaultdict
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from sqlalchemy import select, func, desc, and_
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import and_, desc, func, select
from sqlalchemy.orm import Session
from ...database import get_session from ...database import get_session
from ...database.models import ( from ...database.models import (
Document, Document,
Immeuble, Immeuble,
Lot,
Locataire, Locataire,
Lot,
Revenu, Revenu,
) )

View File

@@ -3,7 +3,7 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ...database import get_session, DatabaseService from ...database import DatabaseService, get_session
from ...services.tag_predictor import TagPredictor from ...services.tag_predictor import TagPredictor
from ..schemas import PredictTagsRequest from ..schemas import PredictTagsRequest

View File

@@ -1,14 +1,14 @@
"""Pydantic schemas for API request/response models.""" """Pydantic schemas for API request/response models."""
from .models import ( from .models import (
SaveRequest,
SaveResponse,
PredictTagsRequest,
DocumentSummary,
DepenseDetail, DepenseDetail,
DepensesSummary, DepensesSummary,
DocumentSummary,
ImmeubleResponse, ImmeubleResponse,
LotResponse, LotResponse,
PredictTagsRequest,
SaveRequest,
SaveResponse,
) )
__all__ = [ __all__ = [

View File

@@ -5,7 +5,6 @@ from typing import Any
from pydantic import BaseModel from pydantic import BaseModel
# ============================================================ # ============================================================
# Requests # Requests
# ============================================================ # ============================================================

View File

@@ -188,10 +188,12 @@ def db_info(db_path: Path | None) -> None:
Montre le nombre de documents, immeubles, lots, etc. Montre le nombre de documents, immeubles, lots, etc.
""" """
from .database import init_db as do_init_db, get_session_factory
from .database.models import Document, Immeuble, Lot, Locataire, Revenu, Depense
from sqlalchemy import func, select from sqlalchemy import func, select
from .database import get_session_factory
from .database import init_db as do_init_db
from .database.models import Depense, Document, Immeuble, Locataire, Lot, Revenu
# Initialize/connect to database # Initialize/connect to database
result_path = do_init_db(db_path) result_path = do_init_db(db_path)
SessionLocal = get_session_factory() SessionLocal = get_session_factory()
@@ -206,8 +208,8 @@ def db_info(db_path: Path | None) -> None:
dep_count = session.execute(select(func.count(Depense.id))).scalar() dep_count = session.execute(select(func.count(Depense.id))).scalar()
click.echo(f"Base de donnees: {result_path}") click.echo(f"Base de donnees: {result_path}")
click.echo(f"") click.echo("")
click.echo(f"Statistiques:") click.echo("Statistiques:")
click.echo(f" - Documents: {doc_count}") click.echo(f" - Documents: {doc_count}")
click.echo(f" - Immeubles: {imm_count}") click.echo(f" - Immeubles: {imm_count}")
click.echo(f" - Lots: {lot_count}") click.echo(f" - Lots: {lot_count}")

View File

@@ -1,9 +1,9 @@
"""Database module for plesna-gerance.""" """Database module for plesna-gerance."""
from .connection import get_engine, get_session, get_session_factory, init_db
from .models import Base, Document, Immeuble, Lot, Locataire, Revenu, Depense, Setting
from .service import DatabaseService, DuplicateDocumentError
from . import storage from . import storage
from .connection import get_engine, get_session, get_session_factory, init_db
from .models import Base, Depense, Document, Immeuble, Locataire, Lot, Revenu, Setting
from .service import DatabaseService, DuplicateDocumentError
__all__ = [ __all__ = [
"get_engine", "get_engine",

View File

@@ -1,8 +1,8 @@
"""Database connection management for SQLite.""" """Database connection management for SQLite."""
import os import os
from collections.abc import Generator
from pathlib import Path from pathlib import Path
from typing import Generator
from sqlalchemy import create_engine, text from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.orm import Session, sessionmaker

View File

@@ -1,19 +1,18 @@
"""SQLAlchemy models for plesna-gerance database.""" """SQLAlchemy models for plesna-gerance database."""
from datetime import date, datetime, timezone from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import ( from sqlalchemy import (
Column, Column,
Integer,
String,
Float,
Date, Date,
DateTime, DateTime,
Text, Float,
ForeignKey, ForeignKey,
UniqueConstraint,
Index, Index,
Integer,
String,
Text,
UniqueConstraint,
) )
from sqlalchemy.orm import DeclarativeBase, relationship from sqlalchemy.orm import DeclarativeBase, relationship

View File

@@ -6,11 +6,10 @@ from typing import Any
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from .models import Document, Immeuble, Lot, Locataire, Revenu, Depense, Tag
from . import storage
from ..utils.amounts import parse_amount from ..utils.amounts import parse_amount
from . import storage
from .models import Depense, Document, Immeuble, Locataire, Lot, Revenu, Tag
class DuplicateDocumentError(Exception): class DuplicateDocumentError(Exception):

View File

@@ -240,7 +240,7 @@ def read_json(relative_path: str, storage_root: Path | None = None) -> dict[str,
FileNotFoundError: If file doesn't exist. FileNotFoundError: If file doesn't exist.
""" """
full_path = get_absolute_path(relative_path, storage_root) full_path = get_absolute_path(relative_path, storage_root)
with open(full_path, "r", encoding="utf-8") as f: with open(full_path, encoding="utf-8") as f:
return json.load(f) return json.load(f)

View File

@@ -1,9 +1,9 @@
"""Orchestrateur principal pour l'extraction des comptes rendus de gérance.""" """Orchestrateur principal pour l'extraction des comptes rendus de gérance."""
from .parsers.pdf import extract_text_from_pdf
from .parsers.metadata import extract_metadata
from .parsers.locataires import extract_situation_locataires from .parsers.locataires import extract_situation_locataires
from .parsers.metadata import extract_metadata
from .parsers.operations import extract_recapitulatif_operations from .parsers.operations import extract_recapitulatif_operations
from .parsers.pdf import extract_text_from_pdf
def extract_compte_rendu(pdf_path: str) -> dict: def extract_compte_rendu(pdf_path: str) -> dict:

View File

@@ -1,9 +1,9 @@
"""Parsers pour les différentes sections des PDFs de gérance.""" """Parsers pour les différentes sections des PDFs de gérance."""
from .pdf import extract_text_from_pdf
from .metadata import extract_metadata
from .locataires import extract_situation_locataires from .locataires import extract_situation_locataires
from .metadata import extract_metadata
from .operations import extract_recapitulatif_operations from .operations import extract_recapitulatif_operations
from .pdf import extract_text_from_pdf
__all__ = [ __all__ = [
"extract_text_from_pdf", "extract_text_from_pdf",

View File

@@ -2,8 +2,8 @@
import re import re
from ..utils.dates import parse_french_date
from ..utils.amounts import extract_amounts_from_line from ..utils.amounts import extract_amounts_from_line
from ..utils.dates import parse_french_date
def _preprocess_locataires_text(text: str) -> str: def _preprocess_locataires_text(text: str) -> str:
@@ -21,7 +21,6 @@ def _preprocess_locataires_text(text: str) -> str:
lines = text.split("\n") lines = text.split("\n")
cleaned_lines = [] cleaned_lines = []
first_situation_found = False first_situation_found = False
in_situation_section = False
# Patterns à ignorer (en-têtes répétés sur chaque page) # Patterns à ignorer (en-têtes répétés sur chaque page)
skip_patterns = [ skip_patterns = [
@@ -64,7 +63,6 @@ def _preprocess_locataires_text(text: str) -> str:
if "SITUATION DES LOCATAIRES" in stripped: if "SITUATION DES LOCATAIRES" in stripped:
if not first_situation_found: if not first_situation_found:
first_situation_found = True first_situation_found = True
in_situation_section = True
cleaned_lines.append(line) cleaned_lines.append(line)
# Ignorer les occurrences suivantes # Ignorer les occurrences suivantes
continue continue

View File

@@ -2,8 +2,8 @@
import re import re
from ..utils.dates import parse_french_date
from ..utils.amounts import parse_amount from ..utils.amounts import parse_amount
from ..utils.dates import parse_french_date
def extract_metadata(text: str) -> dict: def extract_metadata(text: str) -> dict:

View File

@@ -2,10 +2,9 @@
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from ..database import init_db, get_session from ..database import get_session, init_db
from ..database.models import Tag from ..database.models import Tag
# Liste des tags prédéfinis # Liste des tags prédéfinis
PREDEFINED_TAGS = [ PREDEFINED_TAGS = [
"Ascenseur", "Ascenseur",
@@ -57,7 +56,7 @@ def seed_tags():
session.commit() session.commit()
print(f"\n📊 Résumé:") print("\n📊 Résumé:")
print(f" - {created_count} tags créés") print(f" - {created_count} tags créés")
print(f" - {skipped_count} tags existants") print(f" - {skipped_count} tags existants")
print(f" - Total: {len(PREDEFINED_TAGS)} tags") print(f" - Total: {len(PREDEFINED_TAGS)} tags")

View File

@@ -5,9 +5,9 @@ import logging
import httpx import httpx
from .sql_executor import execute_readonly_sql, get_schema_description
from .settings_service import get_setting
from ..database.connection import get_session_factory from ..database.connection import get_session_factory
from .settings_service import get_setting
from .sql_executor import execute_readonly_sql, get_schema_description
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -12,7 +12,6 @@ import sqlite3
from ..database.connection import get_db_path from ..database.connection import get_db_path
# PRAGMA autorisés (lecture de métadonnées uniquement) # PRAGMA autorisés (lecture de métadonnées uniquement)
_ALLOWED_PRAGMAS = {"table_info", "database_list", "table_list"} _ALLOWED_PRAGMAS = {"table_info", "database_list", "table_list"}

View File

@@ -1,9 +1,7 @@
"""Service de prédiction de tags pour les dépenses basé sur l'historique.""" """Service de prédiction de tags pour les dépenses basé sur l'historique."""
from typing import Optional
from collections import Counter
from sqlalchemy import select, func from sqlalchemy import func, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..database.models import Depense, Tag from ..database.models import Depense, Tag
@@ -14,8 +12,8 @@ class TagPrediction:
def __init__( def __init__(
self, self,
tag_id: Optional[int], tag_id: int | None,
tag_name: Optional[str], tag_name: str | None,
confidence: float, confidence: float,
reason: str, reason: str,
): ):
@@ -72,7 +70,7 @@ class TagPredictor:
reason="Aucun historique trouvé", reason="Aucun historique trouvé",
) )
def _predict_by_fournisseur(self, fournisseur: str) -> Optional[TagPrediction]: def _predict_by_fournisseur(self, fournisseur: str) -> TagPrediction | None:
"""Prédit le tag basé sur le fournisseur. """Prédit le tag basé sur le fournisseur.
Retourne le tag le plus fréquemment utilisé pour ce fournisseur. Retourne le tag le plus fréquemment utilisé pour ce fournisseur.
@@ -111,7 +109,7 @@ class TagPredictor:
def _predict_by_sous_categorie( def _predict_by_sous_categorie(
self, sous_categorie: str self, sous_categorie: str
) -> Optional[TagPrediction]: ) -> TagPrediction | None:
"""Prédit le tag basé sur la sous-catégorie. """Prédit le tag basé sur la sous-catégorie.
Retourne le tag le plus fréquemment utilisé pour cette sous-catégorie. Retourne le tag le plus fréquemment utilisé pour cette sous-catégorie.

View File

@@ -1,6 +1,6 @@
"""Utilitaires pour le parsing des données de gérance.""" """Utilitaires pour le parsing des données de gérance."""
from .amounts import extract_amounts_from_line, parse_amount
from .dates import parse_french_date from .dates import parse_french_date
from .amounts import parse_amount, extract_amounts_from_line
__all__ = ["parse_french_date", "parse_amount", "extract_amounts_from_line"] __all__ = ["parse_french_date", "parse_amount", "extract_amounts_from_line"]

View File

@@ -2,7 +2,7 @@
import pytest import pytest
from plesna_gerance.utils.amounts import parse_amount, extract_amounts_from_line from plesna_gerance.utils.amounts import extract_amounts_from_line, parse_amount
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@@ -4,16 +4,16 @@ from datetime import date
import pytest import pytest
from plesna_gerance.database.service import DatabaseService, DuplicateDocumentError
from plesna_gerance.database.models import ( from plesna_gerance.database.models import (
Depense,
Document, Document,
Immeuble, Immeuble,
Lot,
Locataire, Locataire,
Lot,
Revenu, Revenu,
Depense,
Tag, Tag,
) )
from plesna_gerance.database.service import DatabaseService, DuplicateDocumentError
def test_save_document_creates_full_graph(db_session, sample_data): def test_save_document_creates_full_graph(db_session, sample_data):

View File

@@ -5,19 +5,18 @@ import sqlite3
import pytest import pytest
from plesna_gerance.services.sql_executor import ( from plesna_gerance.services.sql_executor import (
execute_readonly_sql,
_validate_sql,
_ensure_limit,
_authorizer,
_SQLITE_OK,
_SQLITE_DENY, _SQLITE_DENY,
_SQLITE_SELECT, _SQLITE_OK,
_SQLITE_READ,
_SQLITE_PRAGMA, _SQLITE_PRAGMA,
_SQLITE_READ,
_SQLITE_SELECT,
MAX_ROWS, MAX_ROWS,
_authorizer,
_ensure_limit,
_validate_sql,
execute_readonly_sql,
) )
# --- Garde lexicale ------------------------------------------------------- # --- Garde lexicale -------------------------------------------------------

View File

@@ -5,9 +5,9 @@ from datetime import date
import pytest import pytest
from plesna_gerance.database.storage import ( from plesna_gerance.database.storage import (
compute_document_paths,
extract_street_letter, extract_street_letter,
sanitize_filename, sanitize_filename,
compute_document_paths,
) )

View File

@@ -6,7 +6,7 @@ import io
import pytest import pytest
from fastapi import UploadFile from fastapi import UploadFile
from plesna_gerance.utils.uploads import read_upload_limited, UploadTooLargeError from plesna_gerance.utils.uploads import UploadTooLargeError, read_upload_limited
def _upload(content: bytes) -> UploadFile: def _upload(content: bytes) -> UploadFile:

31
uv.lock generated
View File

@@ -263,6 +263,7 @@ dependencies = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "pytest" }, { name = "pytest" },
{ name = "ruff" },
] ]
[package.metadata] [package.metadata]
@@ -276,7 +277,10 @@ requires-dist = [
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [{ name = "pytest", specifier = ">=8.0" }] dev = [
{ name = "pytest", specifier = ">=8.0" },
{ name = "ruff", specifier = ">=0.6" },
]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
@@ -529,6 +533,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
] ]
[[package]]
name = "ruff"
version = "0.15.16"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" },
{ url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" },
{ url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" },
{ url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" },
{ url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" },
{ url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" },
{ url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" },
{ url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" },
{ url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" },
{ url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" },
{ url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" },
{ url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" },
{ url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" },
{ url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" },
{ url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" },
{ url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" },
]
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
version = "2.0.45" version = "2.0.45"