diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index c73e032..7a9da52 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.11"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -17,7 +17,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pylint + pip install "pylint>=3.3" + pip install -e ".[dev]" - name: Analysing the code with pylint run: | pylint $(git ls-files '*.py') diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 1168bd9..e14e6ec 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -19,15 +19,15 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v3 with: - python-version: "3.10" + python-version: "3.11" - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install -e ".[dev]" - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..1706f90 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,61 @@ +[MASTER] +# Use multiple processes to speed up Pylint. +jobs=1 + +[MESSAGES CONTROL] +disable= + # Style — already enforced by flake8/ruff + line-too-long, + wrong-import-order, + # Docstrings — not enforced in this project + missing-module-docstring, + missing-class-docstring, + missing-function-docstring, + # Design thresholds — adjusted below + too-few-public-methods, + too-many-arguments, + too-many-positional-arguments, + too-many-return-statements, + too-many-locals, + too-many-branches, + too-many-statements, + # Duplicate code — false positives in UI/template code + duplicate-code, + # Dict literal — cosmetic + use-dict-literal, + # Abstract method bodies — both pass and ... are fine + unnecessary-pass, + unnecessary-ellipsis, + # Broad exception catch is intentional in UI error handlers + broad-exception-caught, + # Lazy imports are an established pattern in this project (navigation, pdf services) + import-outside-toplevel, + # Design rules that are too strict for this codebase + too-many-instance-attributes, + too-many-lines, + too-many-nested-blocks, + # Cosmetic refactor suggestions + unnecessary-comprehension, + simplifiable-condition, + superfluous-parens, + # Naming conventions not followed in this project (D(), N_MAX, donate_url) + invalid-name, + disallowed-name, + +[DESIGN] +max-args = 10 +max-returns = 10 +max-locals = 25 +max-branches = 15 +max-statements = 60 + +[FORMAT] +max-line-length = 127 + +[BASIC] +# Allow single-letter variable names in short scopes +good-names = i,j,k,n,e,f,s,t,x,y,_ + +[SIMILARITIES] +# Minimum lines number of a similarity. +min-similarity-lines = 10 diff --git a/__main__.py b/__main__.py index 7441883..4c58e4c 100644 --- a/__main__.py +++ b/__main__.py @@ -8,6 +8,7 @@ def main() -> None: + """Entry point for the finance-tracker CLI.""" app() diff --git a/finance_tracker/cli.py b/finance_tracker/cli.py index fd1b4d5..8fd4676 100644 --- a/finance_tracker/cli.py +++ b/finance_tracker/cli.py @@ -97,16 +97,14 @@ import typer from sqlmodel import Session, create_engine -from sqlmodel import SQLModel from finance_tracker.config import DATABASE_URL, DOCS_DIR -from finance_tracker.domain.enums import ProductType, QuantityUnit, TransactionType -from finance_tracker.domain.models import Product, RateSchedule, Transaction, Valuation +from finance_tracker.domain.enums import TransactionType +from finance_tracker.domain.models import Transaction, Valuation from finance_tracker.repositories.sqlmodel_repo import ( SQLModelProductRepository, SQLModelTransactionRepository, SQLModelValuationRepository, - SQLModelRateScheduleRepository, init_db, ) from finance_tracker.services.btc_price_service import BTCPriceService, BTCPriceServiceError @@ -173,7 +171,8 @@ def init_db_cmd() -> None: $ finance-tracker init-db ✅ Base de données initialisée """ - init_db() + engine = create_engine(DATABASE_URL, echo=False) + init_db(engine) typer.echo("✅ Base de données initialisée") @@ -432,9 +431,9 @@ def add_transaction(product_name: str = typer.Option(..., help="Nom du produit") if date: try: date_obj = datetime.fromisoformat(date) - except ValueError: + except ValueError as exc: typer.echo("❌ Date invalide (format: YYYY-MM-DD)", err=True) - raise typer.Exit(1) + raise typer.Exit(1) from exc else: date_obj = datetime.utcnow() @@ -518,9 +517,9 @@ def add_valuation(product_name: str = typer.Option(..., help="Nom du produit"), if date: try: date_obj = datetime.fromisoformat(date) - except ValueError: + except ValueError as exc: typer.echo("❌ Date invalide (format: YYYY-MM-DD)", err=True) - raise typer.Exit(1) + raise typer.Exit(1) from exc else: date_obj = datetime.utcnow() @@ -736,9 +735,9 @@ def project(initial_amount: str = typer.Option(10000, help="Montant initial EUR" # Validate frequency try: freq = ProjectionFrequency[frequency] - except KeyError: + except KeyError as exc: typer.echo(f"❌ Fréquence invalide: {frequency}", err=True) - raise typer.Exit(1) + raise typer.Exit(1) from exc # Create projection and calculate projection = ProjectionResult( diff --git a/finance_tracker/domain/models.py b/finance_tracker/domain/models.py index d3792ba..827f8c4 100644 --- a/finance_tracker/domain/models.py +++ b/finance_tracker/domain/models.py @@ -3,7 +3,7 @@ from decimal import Decimal from typing import Optional -from sqlmodel import Column, DateTime, Field, ForeignKey, Numeric, SQLModel +from sqlmodel import Column, DateTime, Field, Numeric, SQLModel from .enums import ProductType, QuantityUnit, TransactionType diff --git a/finance_tracker/i18n/__init__.py b/finance_tracker/i18n/__init__.py index 85b1683..0bba0c1 100644 --- a/finance_tracker/i18n/__init__.py +++ b/finance_tracker/i18n/__init__.py @@ -17,7 +17,7 @@ def detect_language() -> str: try: accept_lang: str = st.context.headers.get("Accept-Language", "fr") return "en" if accept_lang.lower().startswith("en") else "fr" - except Exception: + except (AttributeError, KeyError): return "fr" diff --git a/finance_tracker/i18n/en.py b/finance_tracker/i18n/en.py index 67350d9..8eff242 100644 --- a/finance_tracker/i18n/en.py +++ b/finance_tracker/i18n/en.py @@ -19,7 +19,7 @@ "app.db_init_with_products": "✅ Database initialised with {n} default products", "app.db_init": "✅ Database initialised", "app.export_btn": "📥 Save database (PC)", - "app.no_db_warning": "Please import or create a database to get started.", + "app.sidebar_hint": "Open menu", "app.nav_label": "Navigation", "app.doc_link_btn": "📖 Documentation (README)", "app.donate_btn": "☕ Buy me a Bitcoffee", diff --git a/finance_tracker/i18n/fr.py b/finance_tracker/i18n/fr.py index b57fca6..f55d381 100644 --- a/finance_tracker/i18n/fr.py +++ b/finance_tracker/i18n/fr.py @@ -19,7 +19,7 @@ "app.db_init_with_products": "✅ Base initialisée avec {n} produits par défaut", "app.db_init": "✅ Base initialisée", "app.export_btn": "📥 Sauvegarder la base (PC)", - "app.no_db_warning": "Veuillez importer ou créer une base pour commencer.", + "app.sidebar_hint": "Ouvrir le menu", "app.nav_label": "Navigation", "app.doc_link_btn": "📖 Documentation (README)", "app.donate_btn": "☕ Buy me a Bitcoffee", diff --git a/finance_tracker/repositories/base.py b/finance_tracker/repositories/base.py index 4be1d77..715221f 100644 --- a/finance_tracker/repositories/base.py +++ b/finance_tracker/repositories/base.py @@ -28,7 +28,7 @@ def create(self, product: Product) -> Product: DuplicateProductError If a product with the same name already exists. """ - pass + ... @abstractmethod def get_by_id(self, product_id: int) -> Optional[Product]: @@ -44,7 +44,7 @@ def get_by_id(self, product_id: int) -> Optional[Product]: Product or None Product instance if found, None otherwise. """ - pass + ... @abstractmethod def get_by_name(self, name: str) -> Optional[Product]: @@ -60,7 +60,7 @@ def get_by_name(self, name: str) -> Optional[Product]: Product or None Product instance if found, None otherwise. """ - pass + ... @abstractmethod def get_all(self) -> list[Product]: @@ -71,7 +71,7 @@ def get_all(self) -> list[Product]: list[Product] List of all product instances. """ - pass + ... @abstractmethod def update(self, product: Product) -> Product: @@ -92,7 +92,7 @@ def update(self, product: Product) -> Product: ProductNotFoundError If the product does not exist in the repository. """ - pass + ... @abstractmethod def delete(self, product_id: int) -> bool: @@ -108,7 +108,7 @@ def delete(self, product_id: int) -> bool: bool True if deletion was successful, False otherwise. """ - pass + ... class ITransactionRepository(ABC): @@ -129,7 +129,7 @@ def create(self, transaction: Transaction) -> Transaction: Transaction The created transaction. """ - pass + ... @abstractmethod def get_by_id(self, transaction_id: int) -> Optional[Transaction]: @@ -146,7 +146,7 @@ def get_by_id(self, transaction_id: int) -> Optional[Transaction]: Optional[Transaction] The transaction if found, None otherwise. """ - pass + ... @abstractmethod def get_by_product_id(self, product_id: int) -> list[Transaction]: @@ -163,7 +163,7 @@ def get_by_product_id(self, product_id: int) -> list[Transaction]: list[Transaction] List of transactions associated with the product. """ - pass + ... @abstractmethod def get_all(self) -> list[Transaction]: @@ -175,7 +175,7 @@ def get_all(self) -> list[Transaction]: list[Transaction] List of all transactions in the repository. """ - pass + ... @abstractmethod def get_all_by_type(self, transaction_type: TransactionType) -> list[Transaction]: @@ -192,7 +192,7 @@ def get_all_by_type(self, transaction_type: TransactionType) -> list[Transaction list[Transaction] List of transactions matching the specified type. """ - pass + ... @abstractmethod def update(self, transaction: Transaction) -> Transaction: @@ -209,7 +209,7 @@ def update(self, transaction: Transaction) -> Transaction: Transaction The updated transaction. """ - pass + ... @abstractmethod def delete(self, transaction_id: int) -> bool: @@ -226,7 +226,7 @@ def delete(self, transaction_id: int) -> bool: bool True if the transaction was deleted, False otherwise. """ - pass + ... class IValuationRepository(ABC): @@ -254,7 +254,7 @@ def create(self, valuation: Valuation) -> Valuation: ValidationError If the valuation data is invalid. """ - pass + ... @abstractmethod def get_by_id(self, valuation_id: int) -> Optional[Valuation]: @@ -270,7 +270,7 @@ def get_by_id(self, valuation_id: int) -> Optional[Valuation]: Optional[Valuation] The valuation if found, None otherwise. """ - pass + ... @abstractmethod def get_latest_by_product_id(self, product_id: int) -> Optional[Valuation]: @@ -286,7 +286,7 @@ def get_latest_by_product_id(self, product_id: int) -> Optional[Valuation]: Optional[Valuation] The latest valuation for the product, None if none exists. """ - pass + ... @abstractmethod def get_by_product_id(self, product_id: int) -> list[Valuation]: @@ -302,7 +302,7 @@ def get_by_product_id(self, product_id: int) -> list[Valuation]: list[Valuation] List of all valuations for the product, empty list if none found. """ - pass + ... @abstractmethod def get_all(self) -> list[Valuation]: @@ -313,7 +313,7 @@ def get_all(self) -> list[Valuation]: list[Valuation] Complete list of all stored valuations. """ - pass + ... @abstractmethod def update(self, valuation: Valuation) -> Valuation: @@ -334,7 +334,7 @@ def update(self, valuation: Valuation) -> Valuation: ValidationError If the updated data is invalid. """ - pass + ... @abstractmethod def delete(self, valuation_id: int) -> bool: @@ -350,4 +350,4 @@ def delete(self, valuation_id: int) -> bool: bool True if deletion succeeded, False if valuation not found. """ - pass + ... diff --git a/finance_tracker/repositories/sqlmodel_repo.py b/finance_tracker/repositories/sqlmodel_repo.py index 2f44182..f0043ec 100644 --- a/finance_tracker/repositories/sqlmodel_repo.py +++ b/finance_tracker/repositories/sqlmodel_repo.py @@ -5,8 +5,7 @@ from sqlalchemy import desc, select from sqlmodel import Session, SQLModel -from finance_tracker.config import DATABASE_URL -from finance_tracker.domain.enums import ProductType, TransactionType +from finance_tracker.domain.enums import TransactionType from finance_tracker.domain.models import Product, RateSchedule, Transaction, Valuation from .base import IProductRepository, ITransactionRepository, IValuationRepository diff --git a/finance_tracker/services/btc_price_service.py b/finance_tracker/services/btc_price_service.py index a0f5b02..78b9c54 100644 --- a/finance_tracker/services/btc_price_service.py +++ b/finance_tracker/services/btc_price_service.py @@ -7,7 +7,7 @@ class BTCPriceServiceError(Exception): """Exception service BTC.""" - pass + ... class BTCPriceService: @@ -61,19 +61,19 @@ def get_btc_price_eur(self) -> Decimal: # CoinGecko frequently blocks cloud requests without an API key try: return self._fetch_from_coingecko() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught errors.append(f"CoinGecko: {str(e)}") # Kraken is lenient with datacenter IPs and has no aggressive Cloudflare protection try: return self._fetch_from_kraken() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught errors.append(f"Kraken: {str(e)}") # Binance is reliable but may block entire AWS ranges try: return self._fetch_from_binance() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught errors.append(f"Binance: {str(e)}") # All providers blocked the request - aggregate errors for debugging @@ -108,8 +108,8 @@ def _fetch_from_kraken(self) -> Decimal: # Use Decimal for precise monetary calculations, avoiding float rounding issues return Decimal(str(price)) - except (KeyError, IndexError): - raise ValueError("Structure de réponse Kraken inattendue") + except (KeyError, IndexError) as exc: + raise ValueError("Structure de réponse Kraken inattendue") from exc def _fetch_from_binance(self) -> Decimal: """Fallback 2: Binance API. diff --git a/finance_tracker/services/dashboard_service.py b/finance_tracker/services/dashboard_service.py index e7195eb..1a4f9d3 100644 --- a/finance_tracker/services/dashboard_service.py +++ b/finance_tracker/services/dashboard_service.py @@ -9,13 +9,13 @@ # Local application from finance_tracker.domain.enums import ProductType, TransactionType -from finance_tracker.domain.models import Product, Transaction, Valuation +from finance_tracker.domain.models import Transaction from finance_tracker.repositories.sqlmodel_repo import ( SQLModelProductRepository, SQLModelTransactionRepository, SQLModelValuationRepository, ) -from finance_tracker.utils.money import format_eur, round_decimal, safe_divide +from finance_tracker.utils.money import format_eur, safe_divide PRODUCT_COLORS = { diff --git a/finance_tracker/services/pdf_report_service.py b/finance_tracker/services/pdf_report_service.py index c5587a2..0f7fb86 100644 --- a/finance_tracker/services/pdf_report_service.py +++ b/finance_tracker/services/pdf_report_service.py @@ -8,12 +8,11 @@ # Third-party import matplotlib.pyplot as plt -from weasyprint import HTML, CSS +from weasyprint import HTML # pylint: disable=import-error # Local application from finance_tracker.config import REPORTS_DIR, TEMPLATES_DIR -from finance_tracker.services.dashboard_service import PRODUCT_COLORS, PortfolioData -from finance_tracker.utils.money import format_eur +from finance_tracker.services.dashboard_service import PortfolioData class PDFReportService: @@ -188,14 +187,14 @@ def _generate_allocation_chart(self, products: list) -> str: ax.set_facecolor("white") # Generate blue gradient from light to dark - colors = plt.cm.Blues([0.35, 0.45, 0.55, 0.62, 0.70, 0.78, 0.86, 0.92, 0.97])[:len(labels)] + colors = plt.cm.Blues([0.35, 0.45, 0.55, 0.62, 0.70, 0.78, 0.86, 0.92, 0.97])[:len(labels)] # pylint: disable=no-member def autopct(pct): # Only show percentage if significant (>3%) to avoid visual clutter return f"{pct:.0f}%" if pct >= 3 else "" - wedges, texts, autotexts = ax.pie( + wedges, _texts, _autotexts = ax.pie( sizes, startangle=90, counterclock=False, diff --git a/finance_tracker/services/projection_service.py b/finance_tracker/services/projection_service.py index 8f052e7..36c7c13 100644 --- a/finance_tracker/services/projection_service.py +++ b/finance_tracker/services/projection_service.py @@ -82,7 +82,7 @@ def calculate(self) -> None: year_contributions = Decimal(0) year_gains = Decimal(0) - for period in range(periods_per_year): + for _period in range(periods_per_year): # Add contribution before applying return to capture full period growth current_value += self.monthly_contribution year_contributions += self.monthly_contribution diff --git a/finance_tracker/services/simulation_pdf_service.py b/finance_tracker/services/simulation_pdf_service.py index 0681c25..bb83ea6 100644 --- a/finance_tracker/services/simulation_pdf_service.py +++ b/finance_tracker/services/simulation_pdf_service.py @@ -9,7 +9,7 @@ # Third-party import matplotlib.pyplot as plt import pandas as pd -from weasyprint import HTML, CSS +from weasyprint import HTML # pylint: disable=import-error # Local application from finance_tracker.config import REPORTS_DIR, TEMPLATES_DIR @@ -106,7 +106,6 @@ def _render_html(self, If the template contains invalid syntax. """ from jinja2 import Environment, FileSystemLoader - from finance_tracker.utils.money import format_eur # Template directory must be set at instance level for Jinja2 to locate files env = Environment(loader=FileSystemLoader(self.templates_dir)) @@ -197,10 +196,9 @@ def _generate_metric_chart(self, df_long: pd.DataFrame, metric: str) -> str: """ try: import matplotlib - matplotlib.use("Agg") - import matplotlib.pyplot as plt import matplotlib.ticker as mtick import numpy as np + matplotlib.use("Agg") # Palette: tab20 supports up to 20 distinct colors for multiple product lines N_MAX = 20 @@ -239,7 +237,7 @@ def _generate_metric_chart(self, df_long: pd.DataFrame, metric: str) -> str: else: scale, unit = 1, "" - def fmt_val(v: float, short: bool = False) -> str: + def fmt_val(v: float, short: bool = False) -> str: # pylint: disable=unused-argument """Format a raw numeric value for display. Converts a raw value to a human-readable string representation, @@ -291,8 +289,8 @@ def fmt_val(v: float, short: bool = False) -> str: if n_per_year not in (1, 4, 12): n_per_year = int(df_long.groupby("year")["period"].nunique().mode().iloc[0]) - except Exception: - pass + except Exception: # pylint: disable=broad-exception-caught + n_per_year = 12 # keep default if inference fails max_period = int(df_long["period"].max()) if not df_long.empty else 0 @@ -454,12 +452,12 @@ def fmt_val(v: float, short: bool = False) -> str: return f"data:image/png;base64,{img_b64}" - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught print(f"Erreur génération graphique {metric}: {e}") try: plt.close("all") - except Exception: - pass + except Exception as close_err: # pylint: disable=broad-exception-caught + print(f"Erreur nettoyage matplotlib: {close_err}") return None @@ -497,8 +495,8 @@ def _dataframe_to_html_table(self, df: pd.DataFrame, max_rows: int = 100) -> str display_df[col] = display_df[col].apply( lambda x: f"{float(x):,.0f}".replace(",", " ") if pd.notna(x) else "" ) - except: - pass + except Exception as fmt_err: # pylint: disable=broad-exception-caught + print(f"Erreur formatage colonne {col}: {fmt_err}") html = display_df.to_html( index=False, diff --git a/finance_tracker/services/simulation_service.py b/finance_tracker/services/simulation_service.py index 86990e5..cc51b2d 100644 --- a/finance_tracker/services/simulation_service.py +++ b/finance_tracker/services/simulation_service.py @@ -123,7 +123,7 @@ class PERConfig: Currently empty placeholder for future PER-specific configuration. """ - pass + ... @dataclass diff --git a/finance_tracker/web/app.py b/finance_tracker/web/app.py index 865d498..fced04f 100644 --- a/finance_tracker/web/app.py +++ b/finance_tracker/web/app.py @@ -10,6 +10,7 @@ support for SCPIs, cryptocurrencies like Bitcoin, savings accounts, and other financial assets. """ +import json import streamlit as st import os from finance_tracker.web.db import get_session, get_db_path, get_engine @@ -103,9 +104,9 @@ def render_db_manager(): init_db(engine) # Seed default products for new databases - session = get_session() - created_count = seed_default_products(session) - session.close() + _session = get_session() + created_count = seed_default_products(_session) + _session.close() st.session_state.db_loaded = True @@ -116,8 +117,52 @@ def render_db_manager(): st.rerun() - st.warning(t("app.no_db_warning")) - # Stop execution here - no point loading the rest of the app without a database + # Animated arrow hinting at the sidebar toggle after 20 s of inactivity. + # Injected into document.body via JS so it survives Streamlit re-renders. + hint_label = json.dumps(t("app.sidebar_hint")) + st.markdown(f"""""", unsafe_allow_html=True) + + # Show documentation (needs no DB) so users aren't stranded on a blank page + from finance_tracker.web.views.documentation import render as doc_render + doc_render(None) st.stop() # EXPORT diff --git a/finance_tracker/web/db.py b/finance_tracker/web/db.py index 5474f31..e1a1319 100644 --- a/finance_tracker/web/db.py +++ b/finance_tracker/web/db.py @@ -14,8 +14,6 @@ import streamlit as st from sqlmodel import create_engine, Session -from finance_tracker.config import DATABASE_URL - def get_db_path(): """Generate and return session-specific database file path. diff --git a/finance_tracker/web/views/bitcoin.py b/finance_tracker/web/views/bitcoin.py index c9a9e7f..828eac5 100644 --- a/finance_tracker/web/views/bitcoin.py +++ b/finance_tracker/web/views/bitcoin.py @@ -8,7 +8,7 @@ from finance_tracker.i18n import t -def render(session: Session) -> None: +def render(session: Session) -> None: # pylint: disable=unused-argument """Render the Bitcoin transition page.""" st.title(t("bitcoin.title")) st.info(t("bitcoin.redirect_info")) diff --git a/finance_tracker/web/views/dashboard.py b/finance_tracker/web/views/dashboard.py index 6ad561f..95cdb42 100644 --- a/finance_tracker/web/views/dashboard.py +++ b/finance_tracker/web/views/dashboard.py @@ -180,7 +180,7 @@ def _render_bitcoin_expander(details: dict, product_id: int, service: "Dashboard )) st.success(t("dashboard.btc_snapshot_saved").format(v=format_eur(total_val))) st.rerun() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught st.error(t("dashboard.btc_error").format(e=e)) # ── Recent snapshots table ─────────────────────────────────────────────────── @@ -294,7 +294,7 @@ def _render_generic_expander(details: dict, product_id: int, service: "Dashboard service.valuation_repo.create(val) st.success(t("valuations.added_success")) st.rerun() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught st.error(t("valuations.error").format(e=e)) # ── Editable valuations table ─────────────────────────────────────────────── @@ -352,7 +352,7 @@ def _render_generic_expander(details: dict, product_id: int, service: "Dashboard service.valuation_repo.update(v) st.success(t("valuations.applied_success")) st.rerun() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught st.error(t("valuations.error").format(e=e)) with cb: if st.button(t("valuations.reload_btn"), key=f"val_reload_{product_id}", width="stretch"): @@ -369,7 +369,7 @@ def render(session: Session) -> None: try: portfolio = service.build_portfolio() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught st.error(t("dashboard.load_error").format(e=e)) return @@ -556,7 +556,7 @@ def render(session: Session) -> None: st.session_state[pdf_cache_key] = f.read() st.rerun() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught st.error(t("dashboard.error").format(e=e)) else: # Cached state: show download button @@ -586,7 +586,7 @@ def render(session: Session) -> None: json_data = service.export_json(portfolio) st.session_state[json_cache_key] = json_data st.rerun() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught st.error(t("dashboard.error").format(e=e)) else: # Cached state: show download button diff --git a/finance_tracker/web/views/data_manager.py b/finance_tracker/web/views/data_manager.py index f682dc5..9712f82 100644 --- a/finance_tracker/web/views/data_manager.py +++ b/finance_tracker/web/views/data_manager.py @@ -106,20 +106,18 @@ def _render_edit_tab(session: Session) -> None: st.markdown("---") # Import here to avoid circular dependencies with Streamlit's module loading - from finance_tracker.web.views.products import ( - _edit_transactions, - _edit_valuations, - _edit_products - ) + from finance_tracker.web.views import transactions as tx_view + from finance_tracker.web.views import valuations as val_view + from finance_tracker.web.views import products as prod_view # Route to the appropriate edit function based on user selection if entity_to_edit == "Transactions": - _edit_transactions(session) + tx_view.render(session) elif entity_to_edit == "Valorisations": - _edit_valuations(session) + val_view.render(session) else: - _edit_products(session) + prod_view.render(session) def _add_transaction_form(session: Session) -> None: @@ -183,7 +181,7 @@ def _add_transaction_form(session: Session) -> None: ) tx_repo.create(tx) st.success(f"✅ Transaction ajoutée avec succès sur {product_name} !") - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught # Display user-friendly error instead of crashing st.error(f"❌ Erreur : {e}") @@ -248,7 +246,7 @@ def _add_valuation_form(session: Session) -> None: ) val_repo.create(val) st.success(f"✅ Valorisation de {total_value}€ ajoutée pour {product_name} !") - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught st.error(f"❌ Erreur : {e}") @@ -311,5 +309,5 @@ def _add_product_form(session: Session) -> None: ) product_repo.create(product) st.success(f"✅ Produit '{name}' créé avec succès !") - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught st.error(f"❌ Erreur lors de la création : {e}") diff --git a/finance_tracker/web/views/documentation.py b/finance_tracker/web/views/documentation.py index c4fbb96..2e6b43d 100644 --- a/finance_tracker/web/views/documentation.py +++ b/finance_tracker/web/views/documentation.py @@ -35,7 +35,7 @@ def _load_markdown_file(filename: str) -> str: with open(file_path, "r", encoding="utf-8") as f: return f.read() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught return t("documentation.file_load_error").format(error=str(e)) @@ -585,7 +585,7 @@ def _render_tab_help() -> None: -def render(session: Session) -> None: +def render(_session: Session) -> None: """Render the documentation page.""" st.title(t("documentation.title")) diff --git a/finance_tracker/web/views/products.py b/finance_tracker/web/views/products.py index e62d365..f9139fc 100644 --- a/finance_tracker/web/views/products.py +++ b/finance_tracker/web/views/products.py @@ -28,8 +28,8 @@ def render(session: Session) -> None: # Initialize repositories for database operations product_repo = SQLModelProductRepository(session) - tx_repo = SQLModelTransactionRepository(session) - val_repo = SQLModelValuationRepository(session) + _tx_repo = SQLModelTransactionRepository(session) + _val_repo = SQLModelValuationRepository(session) # ═══════════════════════════════════════════════════════════════════════════ # SECTION 1: ADD PRODUCT FORM @@ -76,7 +76,7 @@ def render(session: Session) -> None: product_repo.create(p) st.success(t("products.created_success")) st.rerun() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught st.error(t("products.error").format(e=e)) st.markdown("---") @@ -199,7 +199,7 @@ def render(session: Session) -> None: st.success(t("products.applied_success")) st.rerun() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught st.error(t("products.error").format(e=e)) with c2: diff --git a/finance_tracker/web/views/simulation.py b/finance_tracker/web/views/simulation.py index bd9bf35..43b2f95 100644 --- a/finance_tracker/web/views/simulation.py +++ b/finance_tracker/web/views/simulation.py @@ -1259,7 +1259,7 @@ def render(session: Session) -> None: st.session_state[pdf_cache_key] = pdf_bytes st.session_state["sim_pdf_needs_update"] = False st.rerun() # Refresh to show download button - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught st.error(t("simulation.pdf_error").format(e=e)) # PDF is ready for download diff --git a/finance_tracker/web/views/transactions.py b/finance_tracker/web/views/transactions.py index 168689a..fa4f088 100644 --- a/finance_tracker/web/views/transactions.py +++ b/finance_tracker/web/views/transactions.py @@ -89,7 +89,7 @@ def render(session: Session) -> None: tx_repo.create(tx) st.success(t("transactions.added_success")) st.rerun() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught st.error(t("transactions.error").format(e=e)) st.markdown("---") @@ -219,7 +219,7 @@ def render(session: Session) -> None: st.success(t("transactions.applied_success")) st.rerun() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught st.error(t("transactions.error").format(e=e)) with c2: diff --git a/finance_tracker/web/views/valuations.py b/finance_tracker/web/views/valuations.py index 63d2257..7a73993 100644 --- a/finance_tracker/web/views/valuations.py +++ b/finance_tracker/web/views/valuations.py @@ -75,7 +75,7 @@ def render(session: Session) -> None: val_repo.create(val) st.success(t("valuations.added_success")) st.rerun() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught st.error(t("valuations.error").format(e=e)) st.markdown("---") @@ -194,7 +194,7 @@ def render(session: Session) -> None: st.success(t("valuations.applied_success")) st.rerun() - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught st.error(t("valuations.error").format(e=e)) with c2: diff --git a/tests/test_dashboard_service.py b/tests/test_dashboard_service.py index 9243f5d..b3f63b2 100644 --- a/tests/test_dashboard_service.py +++ b/tests/test_dashboard_service.py @@ -1,4 +1,5 @@ """Tests for DashboardService per-product methods.""" +# pylint: disable=redefined-outer-name # pytest fixture pattern from datetime import datetime from decimal import Decimal