forked from gaugendre/ofx-processor
Compare commits
25 commits
Author | SHA1 | Date | |
---|---|---|---|
Gabriel Augendre | a4eb23280f | ||
Gabriel Augendre | 85c36bc14f | ||
Gabriel Augendre | 7494f4e51b | ||
Gabriel Augendre | c36030856d | ||
Gabriel Augendre | 3df3d1059c | ||
Gabriel Augendre | 14499eab65 | ||
Gabriel Augendre | 3290134835 | ||
Gabriel Augendre | fb55fa4d1a | ||
Gabriel Augendre | 2e468f69f1 | ||
Gabriel Augendre | 0a4e7985cc | ||
Gabriel Augendre | 01b486031b | ||
Gabriel Augendre | 2e0d5dc419 | ||
Gabriel Augendre | 30f1e1c990 | ||
Gabriel Augendre | a0395fc296 | ||
Gabriel Augendre | 57cdb596c5 | ||
Gabriel Augendre | a54906c6d1 | ||
Gabriel Augendre | 3185a7d103 | ||
Gabriel Augendre | e0006873f9 | ||
Gabriel Augendre | 1a94bd6d36 | ||
Gabriel Augendre | 80a15d6825 | ||
Gabriel Augendre | 36f74681b2 | ||
Gabriel Augendre | b190913e6e | ||
Gabriel Augendre | 9eef4a66a6 | ||
Gabriel Augendre | 6bfb5ca29d | ||
Gabriel Augendre | b831f2be5d |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -228,3 +228,4 @@ dmypy.json
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
.direnv
|
||||||
|
|
1
.tool-versions
Normal file
1
.tool-versions
Normal file
|
@ -0,0 +1 @@
|
||||||
|
python 3.11.2
|
14
Dockerfile
14
Dockerfile
|
@ -1,13 +1,15 @@
|
||||||
FROM debian:bullseye 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.30.0"
|
ARG GECKODRIVER_VERSION="v0.33.0"
|
||||||
ARG GECKODRIVER_FILENAME="geckodriver-$GECKODRIVER_VERSION-linux64"
|
ARG GECKODRIVER_FILENAME="geckodriver-$GECKODRIVER_VERSION-linux64.tar.gz"
|
||||||
RUN wget -q https://github.com/mozilla/geckodriver/releases/download/$GECKODRIVER_VERSION/$GECKODRIVER_FILENAME.tar.gz && tar xvf $GECKODRIVER_FILENAME.tar.gz && rm $GECKODRIVER_FILENAME.tar.gz
|
RUN wget -q https://github.com/mozilla/geckodriver/releases/download/$GECKODRIVER_VERSION/$GECKODRIVER_FILENAME \
|
||||||
|
&& tar xvf $GECKODRIVER_FILENAME \
|
||||||
|
&& rm $GECKODRIVER_FILENAME
|
||||||
|
|
||||||
FROM python:3.10-slim-bullseye as final
|
FROM python:3.11-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
|
||||||
RUN pip install ofx-processor==$OFX_VERSION
|
RUN pip --disable-pip-version-check install --quiet ofx-processor==$OFX_VERSION
|
||||||
CMD ["ynab", "lcl", "--download"]
|
CMD ["ynab", "lcl", "--download"]
|
||||||
|
|
BIN
error_download_lcl.png
Normal file
BIN
error_download_lcl.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 93 KiB |
|
@ -1,8 +1,10 @@
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
|
from selenium.common.exceptions import NoSuchElementException
|
||||||
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,16 +36,24 @@ class LclDownloader:
|
||||||
self.selenium.implicitly_wait(30)
|
self.selenium.implicitly_wait(30)
|
||||||
|
|
||||||
def download(self) -> str:
|
def download(self) -> str:
|
||||||
|
try:
|
||||||
|
return self._download()
|
||||||
|
except Exception:
|
||||||
|
screenshot = Path(self.config.screenshot_dir) / "error_download_lcl.png"
|
||||||
|
self.selenium.save_screenshot(screenshot)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _download(self) -> str:
|
||||||
selenium = self.selenium
|
selenium = self.selenium
|
||||||
|
|
||||||
click.secho("Logging in to LCL...", fg="blue")
|
click.secho("Logging in to LCL...", fg="blue")
|
||||||
selenium.get("https://monespace.lcl.fr/connexion")
|
selenium.get("https://monespace.lcl.fr/connexion")
|
||||||
try:
|
try:
|
||||||
self._click(By.ID, "popin_tc_privacy_button_2")
|
self._click(By.ID, "popin_tc_privacy_button_2")
|
||||||
except:
|
click.secho("Accepting privacy policy...", fg="blue")
|
||||||
# If the user has already accepted the privacy policy,
|
except NoSuchElementException:
|
||||||
# the button is not present
|
click.secho("Privacy policy already accepted", fg="blue")
|
||||||
pass
|
|
||||||
login_input = selenium.find_element(By.ID, "identifier")
|
login_input = selenium.find_element(By.ID, "identifier")
|
||||||
login_input.send_keys(self.config.bank_identifier)
|
login_input.send_keys(self.config.bank_identifier)
|
||||||
self._click(By.CLASS_NAME, "app-cta-button")
|
self._click(By.CLASS_NAME, "app-cta-button")
|
||||||
|
@ -52,6 +62,23 @@ class LclDownloader:
|
||||||
self._click(By.CLASS_NAME, "app-cta-button")
|
self._click(By.CLASS_NAME, "app-cta-button")
|
||||||
click.secho("Logged in!", fg="green")
|
click.secho("Logged in!", fg="green")
|
||||||
|
|
||||||
|
retry = True
|
||||||
|
while retry:
|
||||||
|
try:
|
||||||
|
self._click(By.CSS_SELECTOR, ".app-cta-button--primary")
|
||||||
|
click.secho("Dismissing welcome screen...", fg="blue")
|
||||||
|
time.sleep(1)
|
||||||
|
except NoSuchElementException:
|
||||||
|
click.secho("No welcome screen found.", fg="blue")
|
||||||
|
retry = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._click(By.CSS_SELECTOR, ".burger-menu-content")
|
||||||
|
self._click(By.CSS_SELECTOR, ".return-legacy-button")
|
||||||
|
click.secho("Going back to legacy version...", fg="blue")
|
||||||
|
except NoSuchElementException:
|
||||||
|
click.secho("Probably already on legacy version.", fg="blue")
|
||||||
|
|
||||||
click.secho("Navigating through archives...", fg="blue")
|
click.secho("Navigating through archives...", fg="blue")
|
||||||
self._click(By.ID, "linkSynthese")
|
self._click(By.ID, "linkSynthese")
|
||||||
self._click(By.CLASS_NAME, "picDl")
|
self._click(By.CLASS_NAME, "picDl")
|
||||||
|
|
8
ofx_processor/senders/__init__.py
Normal file
8
ofx_processor/senders/__init__.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from ofx_processor.senders import sms, email, telegram, home_assistant
|
||||||
|
|
||||||
|
SENDERS = {
|
||||||
|
"sms": sms.send,
|
||||||
|
"email": email.send,
|
||||||
|
"telegram": telegram.send,
|
||||||
|
"home_assistant": home_assistant.send,
|
||||||
|
}
|
24
ofx_processor/senders/email.py
Normal file
24
ofx_processor/senders/email.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import click
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from ofx_processor.utils.config import Config
|
||||||
|
|
||||||
|
|
||||||
|
def send(config: Config, amount: Decimal) -> None:
|
||||||
|
if not config.email_setup:
|
||||||
|
click.secho("Email is not properly setup", fg="yellow")
|
||||||
|
return
|
||||||
|
res = requests.post(
|
||||||
|
f"https://api.mailgun.net/v3/{config.mailgun_domain}/messages",
|
||||||
|
auth=("api", config.mailgun_api_key),
|
||||||
|
data={
|
||||||
|
"from": config.mailgun_from,
|
||||||
|
"to": [config.email_recipient],
|
||||||
|
"subject": f"Reconciled balance: {amount}",
|
||||||
|
"text": f"Here's your reconciled balance: {amount}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if res.status_code >= 400:
|
||||||
|
click.secho("Error while sending email", fg="yellow")
|
20
ofx_processor/senders/home_assistant.py
Normal file
20
ofx_processor/senders/home_assistant.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import click
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from ofx_processor.utils.config import Config
|
||||||
|
|
||||||
|
|
||||||
|
def send(config: Config, amount: Decimal) -> None:
|
||||||
|
if not config.home_assistant_setup:
|
||||||
|
click.secho("Home Assistant is not properly setup", fg="yellow")
|
||||||
|
return
|
||||||
|
res = requests.post(
|
||||||
|
config.home_assistant_webhook_url,
|
||||||
|
json={
|
||||||
|
"reconciled": str(amount),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if res.status_code >= 400:
|
||||||
|
click.secho("Error while calling Home Assistant", fg="yellow")
|
22
ofx_processor/senders/sms.py
Normal file
22
ofx_processor/senders/sms.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import click
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from ofx_processor.utils.config import Config
|
||||||
|
|
||||||
|
|
||||||
|
def send(config: Config, amount: Decimal) -> None:
|
||||||
|
if not config.sms_setup:
|
||||||
|
click.secho("SMS is not properly setup", fg="yellow")
|
||||||
|
return
|
||||||
|
res = requests.post(
|
||||||
|
f"https://smsapi.free-mobile.fr/sendmsg",
|
||||||
|
json={
|
||||||
|
"user": config.sms_user,
|
||||||
|
"pass": config.sms_key,
|
||||||
|
"msg": f"Reconciled balance: {amount}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if res.status_code >= 400:
|
||||||
|
click.secho("Error while sending SMS", fg="yellow")
|
24
ofx_processor/senders/telegram.py
Normal file
24
ofx_processor/senders/telegram.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import asyncio
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import click
|
||||||
|
import telegram
|
||||||
|
|
||||||
|
from ofx_processor.utils.config import Config
|
||||||
|
|
||||||
|
|
||||||
|
def send(config: Config, amount: Decimal) -> None:
|
||||||
|
if not config.telegram_setup:
|
||||||
|
click.secho("Telegram is not properly setup", fg="yellow")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
asyncio.run(_send_telegram_message(config.telegram_bot_token, config.telegram_bot_chat_id, f"Reconciled balance: {amount}"))
|
||||||
|
except Exception as e:
|
||||||
|
click.secho(f"Error while sending Telegram message. {type(e).__name__}: {e}", fg="yellow")
|
||||||
|
|
||||||
|
|
||||||
|
async def _send_telegram_message(bot_token: str, chat_id: str, message: str) -> None:
|
||||||
|
bot = telegram.Bot(bot_token)
|
||||||
|
async with bot:
|
||||||
|
await bot.send_message(chat_id=chat_id, text=message)
|
|
@ -2,11 +2,11 @@ import sys
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import requests
|
|
||||||
from ofxtools import OFXTree
|
from ofxtools import OFXTree
|
||||||
from ofxtools.header import OFXHeaderError
|
from ofxtools.header import OFXHeaderError
|
||||||
from ofxtools.models import Aggregate
|
from ofxtools.models import Aggregate
|
||||||
|
|
||||||
|
from ofx_processor.senders import SENDERS
|
||||||
from ofx_processor.utils.base_processor import BaseLine, BaseProcessor
|
from ofx_processor.utils.base_processor import BaseLine, BaseProcessor
|
||||||
from ofx_processor.utils.config import get_config
|
from ofx_processor.utils.config import get_config
|
||||||
|
|
||||||
|
@ -36,10 +36,12 @@ class OfxBaseProcessor(BaseProcessor):
|
||||||
def send_reconciled_amount(self, method):
|
def send_reconciled_amount(self, method):
|
||||||
amount = self._get_reconciled_amount()
|
amount = self._get_reconciled_amount()
|
||||||
click.secho(f"Reconciled balance: {amount}. Sending via {method}...", fg="blue")
|
click.secho(f"Reconciled balance: {amount}. Sending via {method}...", fg="blue")
|
||||||
if method == "email":
|
config = get_config(self.account_name)
|
||||||
self._send_mail(amount)
|
sender = SENDERS.get(method)
|
||||||
elif method == "sms":
|
if sender:
|
||||||
self._send_sms(amount)
|
sender(config, amount)
|
||||||
|
else:
|
||||||
|
click.secho(f"Method not implemented: {method}.", fg="red", bold=True)
|
||||||
|
|
||||||
def _get_reconciled_amount(self) -> Decimal:
|
def _get_reconciled_amount(self) -> Decimal:
|
||||||
ofx = self._parse_file()
|
ofx = self._parse_file()
|
||||||
|
@ -55,36 +57,3 @@ class OfxBaseProcessor(BaseProcessor):
|
||||||
ofx = parser.convert()
|
ofx = parser.convert()
|
||||||
return ofx
|
return ofx
|
||||||
|
|
||||||
def _send_mail(self, amount: Decimal):
|
|
||||||
config = get_config(self.account_name)
|
|
||||||
if not config.email_setup:
|
|
||||||
click.secho("Email is not properly setup", fg="yellow")
|
|
||||||
return
|
|
||||||
res = requests.post(
|
|
||||||
f"https://api.mailgun.net/v3/{config.mailgun_domain}/messages",
|
|
||||||
auth=("api", config.mailgun_api_key),
|
|
||||||
data={
|
|
||||||
"from": config.mailgun_from,
|
|
||||||
"to": [config.email_recipient],
|
|
||||||
"subject": f"Reconciled balance: {amount}",
|
|
||||||
"text": f"Here's your reconciled balance: {amount}",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if res.status_code >= 400:
|
|
||||||
click.secho("Error while sending email", fg="yellow")
|
|
||||||
|
|
||||||
def _send_sms(self, amount: Decimal):
|
|
||||||
config = get_config(self.account_name)
|
|
||||||
if not config.sms_setup:
|
|
||||||
click.secho("SMS is not properly setup", fg="yellow")
|
|
||||||
return
|
|
||||||
res = requests.post(
|
|
||||||
f"https://smsapi.free-mobile.fr/sendmsg",
|
|
||||||
json={
|
|
||||||
"user": config.sms_user,
|
|
||||||
"pass": config.sms_key,
|
|
||||||
"msg": f"Reconciled balance: {amount}",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if res.status_code >= 400:
|
|
||||||
click.secho("Error while sending SMS", fg="yellow")
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import asyncio
|
||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
@ -5,6 +6,7 @@ from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
import telegram
|
||||||
|
|
||||||
DEFAULT_CONFIG_DIR = click.get_app_dir("ofx_processor")
|
DEFAULT_CONFIG_DIR = click.get_app_dir("ofx_processor")
|
||||||
DEFAULT_CONFIG_FILENAME = "config.ini"
|
DEFAULT_CONFIG_FILENAME = "config.ini"
|
||||||
|
@ -15,6 +17,7 @@ def get_default_config():
|
||||||
default_config["DEFAULT"] = {
|
default_config["DEFAULT"] = {
|
||||||
"token": "<YOUR API TOKEN>",
|
"token": "<YOUR API TOKEN>",
|
||||||
"budget": "<YOUR BUDGET ID>",
|
"budget": "<YOUR BUDGET ID>",
|
||||||
|
"screenshot_dir": "/tmp",
|
||||||
"mailgun_api_key": "",
|
"mailgun_api_key": "",
|
||||||
"mailgun_domain": "",
|
"mailgun_domain": "",
|
||||||
"mailgun_from": "",
|
"mailgun_from": "",
|
||||||
|
@ -54,11 +57,27 @@ def show_file_name():
|
||||||
click.echo(config_file)
|
click.echo(config_file)
|
||||||
|
|
||||||
|
|
||||||
|
@config.command("telegram", help="Display the bot's chat ID.")
|
||||||
|
def print_telegram_chat_id():
|
||||||
|
config = get_config("DEFAULT")
|
||||||
|
click.pause("Please start a conversation with your bot, then press any key...")
|
||||||
|
asyncio.run(print_chat_id(config))
|
||||||
|
|
||||||
|
|
||||||
|
async def print_chat_id(config: "Config"):
|
||||||
|
bot = telegram.Bot(config.telegram_bot_token)
|
||||||
|
async with bot:
|
||||||
|
update = (await bot.get_updates())[-1]
|
||||||
|
chat_id = update.message.chat_id
|
||||||
|
click.secho(f"Your chat ID is: {chat_id}", fg="green", bold=True)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Config:
|
class Config:
|
||||||
account: str
|
account: str
|
||||||
budget_id: str
|
budget_id: str
|
||||||
token: str
|
token: str
|
||||||
|
screenshot_dir: str
|
||||||
bank_identifier: Optional[str] = None
|
bank_identifier: Optional[str] = None
|
||||||
bank_password: Optional[str] = None
|
bank_password: Optional[str] = None
|
||||||
mailgun_api_key: Optional[str] = None
|
mailgun_api_key: Optional[str] = None
|
||||||
|
@ -67,6 +86,9 @@ class Config:
|
||||||
email_recipient: Optional[str] = None
|
email_recipient: Optional[str] = None
|
||||||
sms_user: Optional[str] = None
|
sms_user: Optional[str] = None
|
||||||
sms_key: Optional[str] = None
|
sms_key: Optional[str] = None
|
||||||
|
telegram_bot_token: Optional[str] = None
|
||||||
|
telegram_bot_chat_id: Optional[str] = None
|
||||||
|
home_assistant_webhook_url: Optional[str] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def email_setup(self) -> bool:
|
def email_setup(self) -> bool:
|
||||||
|
@ -90,6 +112,25 @@ class Config:
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def telegram_setup(self) -> bool:
|
||||||
|
"""Return true if all fields are setup for telegram."""
|
||||||
|
return all(
|
||||||
|
[
|
||||||
|
self.telegram_bot_token,
|
||||||
|
self.telegram_bot_chat_id,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def home_assistant_setup(self):
|
||||||
|
"""Return true if all fields are setup for home assistant."""
|
||||||
|
return all(
|
||||||
|
[
|
||||||
|
self.home_assistant_webhook_url,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_config(account: str) -> Config:
|
def get_config(account: str) -> Config:
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
|
@ -113,7 +154,11 @@ def get_config(account: str) -> Config:
|
||||||
section = config[account]
|
section = config[account]
|
||||||
budget_id = section["budget"]
|
budget_id = section["budget"]
|
||||||
token = section["token"]
|
token = section["token"]
|
||||||
account = section["account"]
|
screenshot_dir = section.get("screenshot_dir")
|
||||||
|
if account == "DEFAULT":
|
||||||
|
ynab_account_id = ""
|
||||||
|
else:
|
||||||
|
ynab_account_id = section["account"]
|
||||||
bank_identifier = section.get("bank_identifier")
|
bank_identifier = section.get("bank_identifier")
|
||||||
bank_password = section.get("bank_password")
|
bank_password = section.get("bank_password")
|
||||||
mailgun_api_key = section.get("mailgun_api_key")
|
mailgun_api_key = section.get("mailgun_api_key")
|
||||||
|
@ -122,13 +167,17 @@ def get_config(account: str) -> Config:
|
||||||
email_recipient = section.get("email_recipient")
|
email_recipient = section.get("email_recipient")
|
||||||
sms_user = section.get("sms_user")
|
sms_user = section.get("sms_user")
|
||||||
sms_key = section.get("sms_key")
|
sms_key = section.get("sms_key")
|
||||||
|
telegram_bot_token = section.get("telegram_bot_token")
|
||||||
|
telegram_bot_chat_id = section.get("telegram_bot_chat_id")
|
||||||
|
home_assistant_webhook_url = section.get("home_assistant_webhook_url")
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
return handle_config_file_error(config_file, e)
|
return handle_config_file_error(config_file, e)
|
||||||
|
|
||||||
return Config(
|
return Config(
|
||||||
account,
|
ynab_account_id,
|
||||||
budget_id,
|
budget_id,
|
||||||
token,
|
token,
|
||||||
|
screenshot_dir,
|
||||||
bank_identifier,
|
bank_identifier,
|
||||||
bank_password,
|
bank_password,
|
||||||
mailgun_api_key,
|
mailgun_api_key,
|
||||||
|
@ -137,6 +186,9 @@ def get_config(account: str) -> Config:
|
||||||
email_recipient,
|
email_recipient,
|
||||||
sms_user,
|
sms_user,
|
||||||
sms_key,
|
sms_key,
|
||||||
|
telegram_bot_token,
|
||||||
|
telegram_bot_chat_id,
|
||||||
|
home_assistant_webhook_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import pkgutil
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from ofx_processor import processors
|
from ofx_processor import processors
|
||||||
|
from ofx_processor.senders import SENDERS
|
||||||
|
|
||||||
ARG_TO_OPTION = {
|
ARG_TO_OPTION = {
|
||||||
"keep": click.option(
|
"keep": click.option(
|
||||||
|
@ -19,10 +20,9 @@ ARG_TO_OPTION = {
|
||||||
"-s",
|
"-s",
|
||||||
"--send",
|
"--send",
|
||||||
help=(
|
help=(
|
||||||
"Send the reconciled amount via the chosen method. "
|
"Send the reconciled amount via the chosen method."
|
||||||
"Accepted methods: sms, email"
|
|
||||||
),
|
),
|
||||||
default="",
|
type=click.Choice(list(SENDERS.keys()), case_sensitive=False),
|
||||||
show_default=True,
|
show_default=True,
|
||||||
),
|
),
|
||||||
"download": click.option(
|
"download": click.option(
|
||||||
|
|
1690
poetry.lock
generated
1690
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "ofx-processor"
|
name = "ofx-processor"
|
||||||
version = "4.1.0"
|
version = "4.4.6"
|
||||||
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>"]
|
||||||
|
@ -20,28 +20,28 @@ classifiers = [
|
||||||
"Operating System :: MacOS :: MacOS X",
|
"Operating System :: MacOS :: MacOS X",
|
||||||
"Operating System :: Microsoft :: Windows",
|
"Operating System :: Microsoft :: Windows",
|
||||||
"Operating System :: Unix",
|
"Operating System :: Unix",
|
||||||
"Programming Language :: Python :: 3.8",
|
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
"Topic :: Utilities",
|
"Topic :: Utilities",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.8,<4"
|
python = ">=3.10,<4"
|
||||||
ofxtools = "^0.9.4"
|
ofxtools = ">=0.9.4"
|
||||||
click = "^8.0.3"
|
click = ">=8.0.3"
|
||||||
dateparser = "^1.1.0"
|
dateparser = ">=1.1.0"
|
||||||
requests = "^2.24.0"
|
requests = ">=2.24.0"
|
||||||
selenium = "^4.0.0"
|
selenium = ">=4.0.0"
|
||||||
|
python-telegram-bot = {version = ">=20.0a4", allow-prereleases = true}
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^6.0.1"
|
pytest = ">=6.0.1"
|
||||||
pytest-cov = "^3.0.0"
|
pytest-cov = ">=3.0.0"
|
||||||
invoke = "^1.6.0"
|
invoke = ">=2.0.0"
|
||||||
pre-commit = "^2.15.0"
|
pre-commit = ">=2.15.0"
|
||||||
tox = "^3.24.4"
|
tox = ">=3.24.4"
|
||||||
pdbpp = "^0.10.3"
|
pdbpp = ">=0.10.3"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
ynab = 'ofx_processor.main:cli'
|
ynab = 'ofx_processor.main:cli'
|
||||||
|
@ -56,12 +56,12 @@ profile = "black"
|
||||||
legacy_tox_ini = """
|
legacy_tox_ini = """
|
||||||
[tox]
|
[tox]
|
||||||
isolated_build = true
|
isolated_build = true
|
||||||
envlist = py38, py39, py310
|
envlist = py310, py311
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
whitelist_externals = poetry
|
allowlist_externals = poetry
|
||||||
commands =
|
commands =
|
||||||
poetry install --remove-untracked
|
poetry install --sync
|
||||||
poetry run inv test
|
poetry run inv test
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
token = <YOUR API TOKEN>
|
token = <YOUR API TOKEN>
|
||||||
budget = <YOUR BUDGET ID>
|
budget = <YOUR BUDGET ID>
|
||||||
|
screenshot_dir = /tmp
|
||||||
|
|
||||||
[bpvf]
|
[bpvf]
|
||||||
account = <YOUR BPVF ACCOUNT ID>
|
account = <YOUR BPVF ACCOUNT ID>
|
||||||
|
|
Loading…
Reference in a new issue