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]
dev = [
"pytest>=8.0",
"ruff>=0.6",
]
[tool.pytest.ini_options]
@@ -25,6 +26,28 @@ testpaths = ["tests"]
pythonpath = ["src"]
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]
requires = ["hatchling"]
build-backend = "hatchling.build"

View File

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

View File

@@ -1,13 +1,13 @@
"""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 .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 .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__ = [
"extraction_router",

View File

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

View File

@@ -4,13 +4,13 @@ from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session
from ...database import get_session, DatabaseService
from ...database import DatabaseService, get_session
from ...database.models import Tag
from ...services.settings_service import (
SETTINGS_REGISTRY,
delete_setting,
get_all_settings,
set_setting,
delete_setting,
SETTINGS_REGISTRY,
)
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)."""
if key not in SETTINGS_REGISTRY:
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
all_settings = get_all_settings(session)
return all_settings[key]

View File

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

View File

@@ -4,17 +4,17 @@ import json
import logging
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, File, UploadFile, Form
from fastapi.responses import Response, JSONResponse
from sqlalchemy.orm import Session
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from fastapi.responses import JSONResponse, Response
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from ...database import get_session, DatabaseService, storage
from ...database.models import Document, Immeuble, Lot, Locataire, Revenu, Depense
from ...database import DatabaseService, get_session, storage
from ...database.models import Depense, Document, Immeuble, Locataire, Lot, Revenu
from ...database.service import DuplicateDocumentError
from ...extractor import extract_compte_rendu
from ...utils.uploads import read_upload_limited, UploadTooLargeError
from ..schemas import SaveRequest, SaveResponse, DocumentSummary
from ...utils.uploads import UploadTooLargeError, read_upload_limited
from ..schemas import DocumentSummary, SaveRequest, SaveResponse
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 ...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"])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -188,10 +188,12 @@ def db_info(db_path: Path | None) -> None:
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 .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
result_path = do_init_db(db_path)
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()
click.echo(f"Base de donnees: {result_path}")
click.echo(f"")
click.echo(f"Statistiques:")
click.echo("")
click.echo("Statistiques:")
click.echo(f" - Documents: {doc_count}")
click.echo(f" - Immeubles: {imm_count}")
click.echo(f" - Lots: {lot_count}")

View File

@@ -1,9 +1,9 @@
"""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 .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__ = [
"get_engine",

View File

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

View File

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

View File

@@ -6,11 +6,10 @@ from typing import Any
from sqlalchemy import select
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 . import storage
from .models import Depense, Document, Immeuble, Locataire, Lot, Revenu, Tag
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.
"""
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)

View File

@@ -1,9 +1,9 @@
"""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.metadata import extract_metadata
from .parsers.operations import extract_recapitulatif_operations
from .parsers.pdf import extract_text_from_pdf
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."""
from .pdf import extract_text_from_pdf
from .metadata import extract_metadata
from .locataires import extract_situation_locataires
from .metadata import extract_metadata
from .operations import extract_recapitulatif_operations
from .pdf import extract_text_from_pdf
__all__ = [
"extract_text_from_pdf",

View File

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

View File

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

View File

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

View File

@@ -5,9 +5,9 @@ import logging
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 .settings_service import get_setting
from .sql_executor import execute_readonly_sql, get_schema_description
logger = logging.getLogger(__name__)

View File

@@ -12,7 +12,6 @@ import sqlite3
from ..database.connection import get_db_path
# PRAGMA autorisés (lecture de métadonnées uniquement)
_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."""
from typing import Optional
from collections import Counter
from sqlalchemy import select, func
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from ..database.models import Depense, Tag
@@ -14,8 +12,8 @@ class TagPrediction:
def __init__(
self,
tag_id: Optional[int],
tag_name: Optional[str],
tag_id: int | None,
tag_name: str | None,
confidence: float,
reason: str,
):
@@ -72,7 +70,7 @@ class TagPredictor:
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.
Retourne le tag le plus fréquemment utilisé pour ce fournisseur.
@@ -111,7 +109,7 @@ class TagPredictor:
def _predict_by_sous_categorie(
self, sous_categorie: str
) -> Optional[TagPrediction]:
) -> TagPrediction | None:
"""Prédit le tag basé sur la 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."""
from .amounts import extract_amounts_from_line, parse_amount
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"]

View File

@@ -2,7 +2,7 @@
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(

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import io
import pytest
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:

31
uv.lock generated
View File

@@ -263,6 +263,7 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "ruff" },
]
[package.metadata]
@@ -276,7 +277,10 @@ requires-dist = [
]
[package.metadata.requires-dev]
dev = [{ name = "pytest", specifier = ">=8.0" }]
dev = [
{ name = "pytest", specifier = ">=8.0" },
{ name = "ruff", specifier = ">=0.6" },
]
[[package]]
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" },
]
[[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]]
name = "sqlalchemy"
version = "2.0.45"