Compare commits

...

9 commits

Author SHA1 Message Date
b731c2d085 fix mise config 2024-11-30 10:17:21 +01:00
5bb5b38a2c fix headless 2024-11-29 21:40:30 +01:00
f6c14d98b1 fix lcl downloader 2024-11-29 21:37:03 +01:00
03e1196f88 update dependencies 2024-11-29 20:24:53 +01:00
cdf719f195 fix lcl downloader 2024-11-29 20:20:11 +01:00
8f1662a861 fix ofx lcl 2024-10-14 23:08:44 +02:00
c7acfcd099 fix dockerfile AS 2024-10-14 20:20:29 +02:00
d8c9240808 fix download lcl 2024-10-14 20:19:38 +02:00
43b76772e1 update readme 2024-10-14 19:58:25 +02:00
8 changed files with 718 additions and 594 deletions

3
.mise.toml Normal file
View file

@ -0,0 +1,3 @@
[tools]
poetry = {version='latest', pyproject='pyproject.toml'}
python = '3.13' # must be after poetry so the poetry bin is first in PATH

View file

@ -1,2 +0,0 @@
python 3.11.2
poetry latest

View file

@ -1,13 +1,13 @@
FROM debian:bookworm AS downloader FROM debian:bookworm AS downloader
WORKDIR /app WORKDIR /app
RUN apt-get update && apt-get install -y wget RUN apt-get update && apt-get install -y wget
ARG GECKODRIVER_VERSION="v0.33.0" ARG GECKODRIVER_VERSION="v0.35.0"
ARG GECKODRIVER_FILENAME="geckodriver-$GECKODRIVER_VERSION-linux64.tar.gz" ARG GECKODRIVER_FILENAME="geckodriver-$GECKODRIVER_VERSION-linux64.tar.gz"
RUN wget -q https://github.com/mozilla/geckodriver/releases/download/$GECKODRIVER_VERSION/$GECKODRIVER_FILENAME \ RUN wget -q https://github.com/mozilla/geckodriver/releases/download/$GECKODRIVER_VERSION/$GECKODRIVER_FILENAME \
&& tar xvf $GECKODRIVER_FILENAME \ && tar xvf $GECKODRIVER_FILENAME \
&& rm $GECKODRIVER_FILENAME && rm $GECKODRIVER_FILENAME
FROM python:3.11-slim-bookworm as final FROM python:3.13-slim-bookworm AS final
RUN apt-get update && apt-get install -y firefox-esr RUN apt-get update && apt-get install -y firefox-esr
COPY --from=downloader /app/geckodriver /usr/local/bin/geckodriver COPY --from=downloader /app/geckodriver /usr/local/bin/geckodriver
ARG OFX_VERSION ARG OFX_VERSION

View file

@ -50,7 +50,7 @@ inv full-test
poetry version <major/minor/patch> poetry version <major/minor/patch>
git add . git add .
git commit git commit
inv tag <version> inv tag $(poetry version -s)
inv publish publish-docker inv publish publish-docker
``` ```

View file

@ -5,6 +5,7 @@ from pathlib import Path
import click import click
from selenium import webdriver from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver import Keys
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support.select import Select from selenium.webdriver.support.select import Select
@ -34,6 +35,7 @@ class LclDownloader:
) )
self.selenium = webdriver.Firefox(options=options) self.selenium = webdriver.Firefox(options=options)
self.selenium.implicitly_wait(30) self.selenium.implicitly_wait(30)
self.selenium.set_window_size(1280, 4000)
def download(self) -> str: def download(self) -> str:
try: try:
@ -76,16 +78,17 @@ class LclDownloader:
self._click(By.ID, "export-button") self._click(By.ID, "export-button")
end = datetime.date.today() - datetime.timedelta(days=1) end = datetime.date.today() - datetime.timedelta(days=1)
start = end - datetime.timedelta(days=9) start = end - datetime.timedelta(days=30)
self._type_nth(By.CSS_SELECTOR, "input.range-picker-input", 0, start.strftime("%d/%m/%Y")) self._type_date(By.ID, "mat-input-0", start)
self._type_nth(By.CSS_SELECTOR, "input.range-picker-input", 1, end.strftime("%d/%m/%Y")) self._type_date(By.ID, "mat-input-1", end)
self._click(By.CSS_SELECTOR, "ui-desktop-select button") self._click(By.CSS_SELECTOR, "ui-desktop-select button")
self._click_nth(By.CSS_SELECTOR, "ui-select-list ul li", 2) self._click_nth(By.CSS_SELECTOR, "ui-select-list ul li", 2)
self._click(By.CLASS_NAME, "download-button") self._click(By.CLASS_NAME, "download-button")
click.secho("Found it!", fg="green") click.secho("Found it!", fg="green")
time.sleep(5)
selenium.get("about:downloads") selenium.get("about:downloads")
return self._get_last_download_file_name() return self._get_last_download_file_name()
@ -98,8 +101,21 @@ class LclDownloader:
def _select(self, by: By, value: str, index: int): def _select(self, by: By, value: str, index: int):
Select(self.selenium.find_element(by, value)).select_by_index(index) Select(self.selenium.find_element(by, value)).select_by_index(index)
def _type_nth(self, by: By, value: str, idx: int, value_to_type: str): def _type(self, by: By, value: str, value_to_type: str):
self.selenium.find_elements(by, value)[idx].send_keys(value_to_type) self.selenium.find_element(by, value).send_keys(value_to_type)
def _clear(self, by: By, value: str):
self.selenium.find_element(by, value).clear()
def _type_date(self, by: By, value: str, _date: datetime.date):
# The new version of the form doesn't behave nicely when typing the date.
self._type(by, value, "1")
self._type(by, value, Keys.ARROW_LEFT*5)
self._type(by, value, Keys.BACKSPACE*5)
self._type(by, value, _date.strftime("%d/%m"))
self._type(by, value, Keys.ARROW_RIGHT*5)
self._type(by, value, Keys.BACKSPACE*4)
self._type(by, value, _date.strftime("%Y"))
def _get_last_download_file_name(self, wait_seconds: int = 30): def _get_last_download_file_name(self, wait_seconds: int = 30):
end_time = time.time() + wait_seconds end_time = time.time() + wait_seconds

View file

@ -1,5 +1,6 @@
import sys import sys
from datetime import datetime from datetime import datetime
from operator import truediv
import click import click
import dateparser import dateparser
@ -67,19 +68,27 @@ class LclProcessor(OfxBaseProcessor):
click.secho("Couldn't find ofx file", fg="red") click.secho("Couldn't find ofx file", fg="red")
sys.exit(1) sys.exit(1)
if "Content-Type:" in data[0]: new_lines = [line for line in data if is_valid_line(line)]
with open(self.filename, "w") as temp_file:
temp_file.writelines(data[1:]) with open(self.filename, "w") as temp_file:
temp_file.writelines(new_lines)
ofx = super()._parse_file() ofx = super()._parse_file()
if "Content-Type:" in data[0]: with open(self.filename, "w") as temp_file:
with open(self.filename, "w") as temp_file: temp_file.writelines(data)
temp_file.writelines(data)
return ofx return ofx
def is_valid_line(line):
if "Content-Type:" in line:
return False
if "MKTGINFO" in line:
return False
return True
def main(filename, keep, download, send_method, push_to_ynab): def main(filename, keep, download, send_method, push_to_ynab):
"""Import LCL bank statement (OFX file).""" """Import LCL bank statement (OFX file)."""
if download: if download:
@ -91,6 +100,7 @@ def main(filename, keep, download, send_method, push_to_ynab):
) )
filename = LclDownloader().download() filename = LclDownloader().download()
processor = LclProcessor(filename) processor = LclProcessor(filename)
processor.parse_file()
if send_method: if send_method:
processor.send_reconciled_amount(send_method) processor.send_reconciled_amount(send_method)
if push_to_ynab: if push_to_ynab:

1251
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "ofx-processor" name = "ofx-processor"
version = "4.5.0" version = "4.5.5"
description = "Personal ofx processor" description = "Personal ofx processor"
readme = "README.md" readme = "README.md"
authors = ["Gabriel Augendre <gabriel@augendre.info>"] authors = ["Gabriel Augendre <gabriel@augendre.info>"]