From 6f7e52a42bf7ed040912da0dcd058a9dd73ffaea Mon Sep 17 00:00:00 2001 From: Julien Palard Date: Thu, 28 Mar 2024 08:56:51 +0100 Subject: [PATCH] Initial commit. --- .gitignore | 3 + LICENSE | 21 +++ README.md | 128 +++++++++++++++ boursobank.py | 384 ++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 22 +++ tests/test_parse.py | 88 ++++++++++ 6 files changed, 646 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 boursobank.py create mode 100644 pyproject.toml create mode 100644 tests/test_parse.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b0b98c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +.venv/ +.envrc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cebbece --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 Julien Palard + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f95cd99 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# Parseur de relevés BoursoBank + +⚠ Cette bibliothèque a été développée indémendament de BoursoBank. + + +## Installation + + pip install boursobank + + +## Sécurité + +### Mot de passe + +Cette bibliothèque ne **se connecte pas à internet** (dans le doute, +lis le code) elle ne fait que lire des relevés au format PDF déjà +téléchargés, tous les traitements sont effectés en local. + +Dans le doute il doit être possible de faire tourner l’application +dans [firejail](https://github.com/netblue30/firejail) ou similaire. + +Il n’est donc pas nécessaire de s’inquiéter pour son mot de passe : il +n’est pas demandé (là, pas besoin de relire le code : si la lib ne +demande pas le mot de passe… elle ne l’a pas). + + +### Erreurs du parseur + +Lire des PDF [n’est pas simple](https://pypdf.readthedocs.io/en/stable/user/extract-text.html#ocr-vs-text-extraction). + +Pour s’assurer de ne pas introduire d’erreur dans vos analyses, cette +bibliothèque fournit une méthode `validate()` qui valide que le +montant initial + toutes les lignes donne bien le montant final, sans +quoi une `ValueError` est levée. + +Cet exemple ne lévera donc une exception qu’en cas d’erreur d’analyse +(ou de la banque, comme au monopoly) : + +```python +for file in args.files: + statement = Statement.from_pdf(file) + statement.pretty_print() + statement.validate() +``` + + +## Interface en ligne de commande + +Cette lib est utilisable en ligne de commande : + + boursobank *.pdf + +vous affichera vos relevés (CB ou compte), exemple : + + $ boursobank 2024-01.pdf + 2024-01.pdf + ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Date ┃ RIB ┃ + ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ + │ 2024-01-01 │ 12345 12345 00000000000 99 │ + └────────────┴────────────────────────────┘ + ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓ + ┃ Label ┃ Value ┃ + ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩ + │ VIR SEPA Truc │ 42.42 │ + │ VIR SEPA Machin truc │ 99.00 │ + │ Relevé différé Carte 4810********0000 │ -123.45 │ + └──────────────────────────────────────────┴──────────┘ + + +## API + +Tout l’intérêt est de pouvoir consulter ses relevés en Python, par +exemple un export en CSV : + +``` +import argparse +import csv +import sys +from pathlib import Path + +from boursobank import Statement + + +def main(): + args = parse_args() + statement = Statement.from_pdf(args.ifile) + writer = csv.writer(sys.stdout) + for line in statement.lines: + writer.writerow((line.label, line.value)) + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("ifile", type=Path, help="PDF file") + return parser.parse_args() + + +if __name__ == "__main__": + main() +``` + +La bibliothèque ne fournit qu’un point d’entrée : la classe `Statement` + +Depuis cette classe il est possible de parser des PDF : + + relevé_bancaire = Statement.from_pdf("test.pdf") + +ou du texte : + + relevé_bancaire = Statement.from_text("blah blah") + + +Cette classe fournit principalement deux attributs, un dictionnaire `headers` contenant : + +- `date` : le 1° jour du mois couvert par ce relevé. +- `emit_date` : la date à laquelle le relevé a été rédigé. +- `RIB` : le RIB/IBAN du relevé. +- `devise` : probablement `"EUR"`. +- `card_number` : le numéro de carte bleu si c’est un relevé de carte. +- `card_owner` : le nom du possesseur de la carte bleu si c’est un relevé de carte. + +et un attribut lines contenant des instances de la classe `Line` dont +les attributs principaux sont : + +- `label` : la description courte de la ligne. +- `description` : la suite de la description de la ligne si elle est sur plusieurs lignes. +- `value` : le montant de la ligne (positif pour un crédit, négatif pour un débit). diff --git a/boursobank.py b/boursobank.py new file mode 100644 index 0000000..1048f62 --- /dev/null +++ b/boursobank.py @@ -0,0 +1,384 @@ +"""Parses BoursoBank account statements.""" + +import datetime as dt +import logging +import re +from decimal import Decimal + +from pypdf import PdfReader +from rich.console import Console +from rich.table import Table +from rich import print as rich_print +from rich.panel import Panel + +__version__ = "0.1" + +DATE_RE = r"([0-9]{1,2}/[0-9]{2}/[0-9]{2,4})" + +HEADER_VALUE_PATTERN = rf"""\s* + (?P{DATE_RE})\s+ + (?P[0-9]{{5}}\s+[0-9]{{5}}\s+[0-9]{{11}}\s+[0-9]{{2}})\s+ + ( + (?P[A-Z]{{3}}) + | + (?P[0-9]{{4}}\*{{8}}[0-9]{{4}}) + )\s+ + (?P(du)?\s+{DATE_RE}\s+(au\s+)?{DATE_RE})\s+ + """ + +RE_CARD_OWNER = [ # First pattern is tried first + re.compile(r"Porteur\s+de\s+la\s+carte\s+:\s+(?P.*)$", flags=re.M), + re.compile( + r"44\s+rue\s+Traversiere\s+CS\s+80134\s+92772\s+" + r"Boulogne-Billancourt\s+Cedex\s+(?P.*)$", + flags=re.M, + ), +] + + +logger = logging.getLogger(__name__) + + +def parse_decimal(value: str): + """Parse a French value like 1.234,56 to a Decimal instance.""" + return Decimal(value.replace(".", "").replace(",", ".")) + + +class Line: + """Represents one line (debit or credit) in a bank statement.""" + + PATTERN = re.compile( + rf"\s+(?P{DATE_RE})\s*(?P