forked from gaugendre/ofx-processor
Add YNAB import
This commit is contained in:
parent
b4dbd983e7
commit
6bc72468de
6 changed files with 212 additions and 20 deletions
|
@ -1,12 +1,15 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from ofxtools.Parser import OFXTree
|
from ofxtools.Parser import OFXTree
|
||||||
from ofxtools.header import make_header
|
from ofxtools.header import make_header
|
||||||
|
|
||||||
|
from ofx_processor.utils import ynab
|
||||||
|
|
||||||
|
|
||||||
def _process_name_and_memo(name, memo):
|
def _process_name_and_memo(name, memo):
|
||||||
if "CB****" in name:
|
if "CB****" in name:
|
||||||
|
@ -30,7 +33,13 @@ def process_name_and_memo(transaction):
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.argument("ofx_filename")
|
@click.argument("ofx_filename")
|
||||||
def cli(ofx_filename):
|
@click.option(
|
||||||
|
"--ynab/--file-only",
|
||||||
|
"push_to_ynab",
|
||||||
|
default=False,
|
||||||
|
help="Push data directly to YNAB instead of just writing a file.",
|
||||||
|
)
|
||||||
|
def cli(ofx_filename, push_to_ynab):
|
||||||
parser = OFXTree()
|
parser = OFXTree()
|
||||||
try:
|
try:
|
||||||
parser.parse(ofx_filename)
|
parser.parse(ofx_filename)
|
||||||
|
@ -44,6 +53,9 @@ def cli(ofx_filename):
|
||||||
click.secho("Couldn't parse ofx file", fg="red")
|
click.secho("Couldn't parse ofx file", fg="red")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
ynab_transactions = []
|
||||||
|
transaction_ids = defaultdict(int)
|
||||||
|
|
||||||
for transaction in ofx.statements[0].transactions:
|
for transaction in ofx.statements[0].transactions:
|
||||||
transaction.name, transaction.memo, edited = process_name_and_memo(transaction)
|
transaction.name, transaction.memo, edited = process_name_and_memo(transaction)
|
||||||
|
|
||||||
|
@ -55,15 +67,35 @@ def cli(ofx_filename):
|
||||||
fg="blue",
|
fg="blue",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
date = transaction.dtposted.isoformat().split("T")[0]
|
||||||
|
amount = int(transaction.trnamt * 1000)
|
||||||
|
import_id = f"YNAB:{amount}:{date}"
|
||||||
|
transaction_ids[import_id] += 1
|
||||||
|
occurrence = transaction_ids[import_id]
|
||||||
|
import_id = f"{import_id}:{occurrence}"
|
||||||
|
|
||||||
|
ynab_transactions.append(
|
||||||
|
{
|
||||||
|
"date": date,
|
||||||
|
"amount": amount,
|
||||||
|
"payee_name": transaction.name,
|
||||||
|
"memo": transaction.memo,
|
||||||
|
"import_id": import_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
click.secho(f"Processed {len(ynab_transactions)} transactions total.", fg="blue")
|
||||||
|
|
||||||
header = str(make_header(version=102))
|
header = str(make_header(version=102))
|
||||||
root = ofx.to_etree()
|
root = ofx.to_etree()
|
||||||
data = ElementTree.tostring(root).decode()
|
data = ElementTree.tostring(root).decode()
|
||||||
|
|
||||||
processed_file = os.path.join(os.path.dirname(ofx_filename), "processed.ofx")
|
processed_file = os.path.join(os.path.dirname(ofx_filename), "processed.ofx")
|
||||||
with open(processed_file, "w") as f:
|
with open(processed_file, "w") as f:
|
||||||
f.write(header + data)
|
f.write(header + data)
|
||||||
click.secho("{} written".format(processed_file), fg="green")
|
click.secho("{} written".format(processed_file), fg="green")
|
||||||
|
|
||||||
|
if push_to_ynab:
|
||||||
|
ynab.push_transactions(ynab_transactions, "bpvf")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cli()
|
cli()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import csv
|
import csv
|
||||||
import os
|
import os
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import dateparser
|
import dateparser
|
||||||
|
@ -34,34 +35,64 @@ def process_outflow(line):
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.argument("csv_filename")
|
@click.argument("csv_filename")
|
||||||
def cli(csv_filename):
|
@click.option(
|
||||||
|
"--ynab/--file-only",
|
||||||
|
default=False,
|
||||||
|
help="Push data directly to YNAB instead of just writing a file.",
|
||||||
|
)
|
||||||
|
def cli(csv_filename, ynab):
|
||||||
formatted_data = []
|
formatted_data = []
|
||||||
|
ynab_transactions = []
|
||||||
|
transaction_ids = defaultdict(int)
|
||||||
|
|
||||||
with open(csv_filename) as f:
|
with open(csv_filename) as f:
|
||||||
reader = csv.DictReader(f, delimiter=";")
|
reader = csv.DictReader(f, delimiter=";")
|
||||||
for line in reader:
|
for line in reader:
|
||||||
|
date = process_date(line)
|
||||||
|
payee = line["Reference"]
|
||||||
|
memo = process_memo(line)
|
||||||
|
outflow = process_outflow(line)
|
||||||
|
inflow = process_inflow(line)
|
||||||
formatted_data.append(
|
formatted_data.append(
|
||||||
{
|
{
|
||||||
"Date": process_date(line),
|
"Date": date,
|
||||||
"Payee": line["Reference"],
|
"Payee": payee,
|
||||||
"Memo": process_memo(line),
|
"Memo": memo,
|
||||||
"Outflow": process_outflow(line),
|
"Outflow": outflow,
|
||||||
"Inflow": process_inflow(line),
|
"Inflow": inflow,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
amount = outflow if outflow else inflow
|
||||||
|
amount *= 1000
|
||||||
|
import_id = f"YNAB:{amount}:{date}"
|
||||||
|
transaction_ids[import_id] += 1
|
||||||
|
occurrence = transaction_ids[import_id]
|
||||||
|
import_id = f"{import_id}:{occurrence}"
|
||||||
|
ynab_transactions.append(
|
||||||
|
{
|
||||||
|
"date": date,
|
||||||
|
"amount": amount,
|
||||||
|
"payee_name": payee,
|
||||||
|
"memo": memo,
|
||||||
|
"import_id": import_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not formatted_data:
|
if formatted_data:
|
||||||
|
processed_file = os.path.join(os.path.dirname(csv_filename), "processed.csv")
|
||||||
|
with open(processed_file, "w") as f:
|
||||||
|
writer = csv.DictWriter(
|
||||||
|
f, delimiter=",", quotechar='"', fieldnames=formatted_data[0].keys()
|
||||||
|
)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(formatted_data)
|
||||||
|
|
||||||
|
click.secho("{} written".format(processed_file), fg="green")
|
||||||
|
else:
|
||||||
click.secho("Nothing to write.")
|
click.secho("Nothing to write.")
|
||||||
|
|
||||||
processed_file = os.path.join(os.path.dirname(csv_filename), "processed.csv")
|
if ynab and ynab_transactions:
|
||||||
with open(processed_file, "w") as f:
|
ynab.push_transactions(ynab_transactions, "revolut")
|
||||||
writer = csv.DictWriter(
|
|
||||||
f, delimiter=",", quotechar='"', fieldnames=formatted_data[0].keys()
|
|
||||||
)
|
|
||||||
writer.writeheader()
|
|
||||||
writer.writerows(formatted_data)
|
|
||||||
|
|
||||||
click.secho("{} written".format(processed_file), fg="green")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
0
ofx_processor/utils/__init__.py
Normal file
0
ofx_processor/utils/__init__.py
Normal file
54
ofx_processor/utils/ynab.py
Normal file
54
ofx_processor/utils/ynab.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import configparser
|
||||||
|
import os
|
||||||
|
|
||||||
|
import click
|
||||||
|
import requests
|
||||||
|
|
||||||
|
BASE_URL = "https://api.youneedabudget.com/v1"
|
||||||
|
DEFAULT_CONFIG_DIR = click.get_app_dir("ofx_processor")
|
||||||
|
DEFAULT_CONFIG_FILENAME = "config.ini"
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_config():
|
||||||
|
default_config = configparser.ConfigParser()
|
||||||
|
default_config["DEFAULT"] = {
|
||||||
|
"token": "<YOUR API TOKEN>",
|
||||||
|
"budget": "<YOUR BUDGET ID>",
|
||||||
|
}
|
||||||
|
default_config["bpvf"] = {"account": "<YOUR ACCOUNT ID>"}
|
||||||
|
default_config["revolut"] = {"account": "<YOUR ACCOUNT ID>"}
|
||||||
|
|
||||||
|
return default_config
|
||||||
|
|
||||||
|
|
||||||
|
def push_transactions(transactions, account):
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config_file = os.path.join(DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME)
|
||||||
|
if not os.path.isfile(config_file):
|
||||||
|
os.makedirs(DEFAULT_CONFIG_DIR, exist_ok=True)
|
||||||
|
config = get_default_config()
|
||||||
|
with open(config_file, "w") as file_:
|
||||||
|
config.write(file_)
|
||||||
|
click.secho("Editing config file...")
|
||||||
|
click.pause()
|
||||||
|
click.edit(filename=config_file)
|
||||||
|
|
||||||
|
config.read(config_file)
|
||||||
|
section = config[account]
|
||||||
|
budget_id = section["budget"]
|
||||||
|
url = f"{BASE_URL}/budgets/{budget_id}/transactions"
|
||||||
|
for transaction in transactions:
|
||||||
|
transaction["account_id"] = section["account"]
|
||||||
|
data = {"transactions": transactions}
|
||||||
|
token = section["token"]
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
res = requests.post(url, json=data, headers=headers)
|
||||||
|
res.raise_for_status()
|
||||||
|
data = res.json()["data"]
|
||||||
|
created = data["transactions"]
|
||||||
|
duplicates = data["duplicate_import_ids"]
|
||||||
|
click.secho(f"{len(created)} transactions created in YNAB.", fg="green", bold=True)
|
||||||
|
if duplicates:
|
||||||
|
click.secho(
|
||||||
|
f"{len(duplicates)} transactions ignored (duplicates).", fg="yellow"
|
||||||
|
)
|
76
poetry.lock
generated
76
poetry.lock
generated
|
@ -49,6 +49,22 @@ typed-ast = ">=1.4.0"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
|
name = "certifi"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
version = "2019.11.28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "Universal encoding detector for Python 2 and 3"
|
||||||
|
name = "chardet"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
version = "3.0.4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
description = "Composable command line interface toolkit"
|
description = "Composable command line interface toolkit"
|
||||||
|
@ -80,6 +96,14 @@ pytz = "*"
|
||||||
regex = "*"
|
regex = "*"
|
||||||
tzlocal = "*"
|
tzlocal = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
|
name = "idna"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
version = "2.8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "Read metadata from Python packages"
|
description = "Read metadata from Python packages"
|
||||||
|
@ -216,6 +240,24 @@ optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
version = "2020.1.8"
|
version = "2020.1.8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "Python HTTP for Humans."
|
||||||
|
name = "requests"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
version = "2.22.0"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
certifi = ">=2017.4.17"
|
||||||
|
chardet = ">=3.0.2,<3.1.0"
|
||||||
|
idna = ">=2.5,<2.9"
|
||||||
|
urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"]
|
||||||
|
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
description = "Python 2 and 3 compatibility utilities"
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
|
@ -251,6 +293,18 @@ version = "2.0.0"
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pytz = "*"
|
pytz = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
|
name = "urllib3"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
version = "1.22"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
||||||
|
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "Measures number of Terminal column cells of wide-character codes"
|
description = "Measures number of Terminal column cells of wide-character codes"
|
||||||
|
@ -276,7 +330,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
|
||||||
testing = ["pathlib2", "contextlib2", "unittest2"]
|
testing = ["pathlib2", "contextlib2", "unittest2"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "be08d3690daf99f4cf8210564a85c7346366473c6bfc2f098dcdf3108883c20b"
|
content-hash = "0e7a5824506e425f34a1b69d9cd6715b1ab11ef940b18fef10abc8c429cd638a"
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
|
@ -296,6 +350,14 @@ black = [
|
||||||
{file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
|
{file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
|
||||||
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
|
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
|
||||||
]
|
]
|
||||||
|
certifi = [
|
||||||
|
{file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"},
|
||||||
|
{file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"},
|
||||||
|
]
|
||||||
|
chardet = [
|
||||||
|
{file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
|
||||||
|
{file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
|
||||||
|
]
|
||||||
click = [
|
click = [
|
||||||
{file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"},
|
{file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"},
|
||||||
{file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"},
|
{file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"},
|
||||||
|
@ -308,6 +370,10 @@ dateparser = [
|
||||||
{file = "dateparser-0.7.2-py2.py3-none-any.whl", hash = "sha256:983d84b5e3861cb0aa240cad07f12899bb10b62328aae188b9007e04ce37d665"},
|
{file = "dateparser-0.7.2-py2.py3-none-any.whl", hash = "sha256:983d84b5e3861cb0aa240cad07f12899bb10b62328aae188b9007e04ce37d665"},
|
||||||
{file = "dateparser-0.7.2.tar.gz", hash = "sha256:e1eac8ef28de69a554d5fcdb60b172d526d61924b1a40afbbb08df459a36006b"},
|
{file = "dateparser-0.7.2.tar.gz", hash = "sha256:e1eac8ef28de69a554d5fcdb60b172d526d61924b1a40afbbb08df459a36006b"},
|
||||||
]
|
]
|
||||||
|
idna = [
|
||||||
|
{file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"},
|
||||||
|
{file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"},
|
||||||
|
]
|
||||||
importlib-metadata = [
|
importlib-metadata = [
|
||||||
{file = "importlib_metadata-1.4.0-py2.py3-none-any.whl", hash = "sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359"},
|
{file = "importlib_metadata-1.4.0-py2.py3-none-any.whl", hash = "sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359"},
|
||||||
{file = "importlib_metadata-1.4.0.tar.gz", hash = "sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"},
|
{file = "importlib_metadata-1.4.0.tar.gz", hash = "sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"},
|
||||||
|
@ -374,6 +440,10 @@ regex = [
|
||||||
{file = "regex-2020.1.8-cp38-cp38-win_amd64.whl", hash = "sha256:e7c7661f7276507bce416eaae22040fd91ca471b5b33c13f8ff21137ed6f248c"},
|
{file = "regex-2020.1.8-cp38-cp38-win_amd64.whl", hash = "sha256:e7c7661f7276507bce416eaae22040fd91ca471b5b33c13f8ff21137ed6f248c"},
|
||||||
{file = "regex-2020.1.8.tar.gz", hash = "sha256:d0f424328f9822b0323b3b6f2e4b9c90960b24743d220763c7f07071e0778351"},
|
{file = "regex-2020.1.8.tar.gz", hash = "sha256:d0f424328f9822b0323b3b6f2e4b9c90960b24743d220763c7f07071e0778351"},
|
||||||
]
|
]
|
||||||
|
requests = [
|
||||||
|
{file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"},
|
||||||
|
{file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"},
|
||||||
|
]
|
||||||
six = [
|
six = [
|
||||||
{file = "six-1.13.0-py2.py3-none-any.whl", hash = "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd"},
|
{file = "six-1.13.0-py2.py3-none-any.whl", hash = "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd"},
|
||||||
{file = "six-1.13.0.tar.gz", hash = "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"},
|
{file = "six-1.13.0.tar.gz", hash = "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"},
|
||||||
|
@ -410,6 +480,10 @@ tzlocal = [
|
||||||
{file = "tzlocal-2.0.0-py2.py3-none-any.whl", hash = "sha256:11c9f16e0a633b4b60e1eede97d8a46340d042e67b670b290ca526576e039048"},
|
{file = "tzlocal-2.0.0-py2.py3-none-any.whl", hash = "sha256:11c9f16e0a633b4b60e1eede97d8a46340d042e67b670b290ca526576e039048"},
|
||||||
{file = "tzlocal-2.0.0.tar.gz", hash = "sha256:949b9dd5ba4be17190a80c0268167d7e6c92c62b30026cf9764caf3e308e5590"},
|
{file = "tzlocal-2.0.0.tar.gz", hash = "sha256:949b9dd5ba4be17190a80c0268167d7e6c92c62b30026cf9764caf3e308e5590"},
|
||||||
]
|
]
|
||||||
|
urllib3 = [
|
||||||
|
{file = "urllib3-1.22-py2.py3-none-any.whl", hash = "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b"},
|
||||||
|
{file = "urllib3-1.22.tar.gz", hash = "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"},
|
||||||
|
]
|
||||||
wcwidth = [
|
wcwidth = [
|
||||||
{file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"},
|
{file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"},
|
||||||
{file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"},
|
{file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "ofx-processor"
|
name = "ofx-processor"
|
||||||
version = "0.2.2"
|
version = "0.3.4"
|
||||||
description = "Personal ofx processor"
|
description = "Personal ofx processor"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
|
@ -15,6 +15,7 @@ python = ">=3.7"
|
||||||
ofxtools = "^0.8.20"
|
ofxtools = "^0.8.20"
|
||||||
click = "^7.0"
|
click = "^7.0"
|
||||||
dateparser = "^0.7.2"
|
dateparser = "^0.7.2"
|
||||||
|
requests = "^2.22.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^5.2"
|
pytest = "^5.2"
|
||||||
|
|
Loading…
Reference in a new issue