From 96fbeb257b5ef5585a80b8ad9cf185989c0e17e4 Mon Sep 17 00:00:00 2001 From: Gabriel Augendre Date: Sat, 20 Nov 2021 15:00:43 +0100 Subject: [PATCH] Add LCL automatic file download --- .gitignore | 1 + ofx_processor/downloaders/__init__.py | 1 + ofx_processor/downloaders/lcl.py | 82 ++++++++ ofx_processor/main.py | 5 +- ofx_processor/processors/lcl.py | 11 +- ofx_processor/utils/base_processor.py | 2 +- ofx_processor/utils/config.py | 93 +++++++++ ofx_processor/utils/utils.py | 34 +++- ofx_processor/utils/ynab.py | 80 +------- poetry.lock | 278 +++++++++++++++++++++++++- pyproject.toml | 1 + tests/test_end_to_end.py | 80 +++++--- tests/test_ynab_integration.py | 4 +- 13 files changed, 557 insertions(+), 115 deletions(-) create mode 100644 ofx_processor/downloaders/__init__.py create mode 100644 ofx_processor/downloaders/lcl.py create mode 100644 ofx_processor/utils/config.py diff --git a/.gitignore b/.gitignore index 5fd1ce7..c8e8c00 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +geckodriver.log # Created by https://www.gitignore.io/api/osx,pycharm,python # Edit at https://www.gitignore.io/?templates=osx,pycharm,python diff --git a/ofx_processor/downloaders/__init__.py b/ofx_processor/downloaders/__init__.py new file mode 100644 index 0000000..a7054cb --- /dev/null +++ b/ofx_processor/downloaders/__init__.py @@ -0,0 +1 @@ +from .lcl import LclDownloader diff --git a/ofx_processor/downloaders/lcl.py b/ofx_processor/downloaders/lcl.py new file mode 100644 index 0000000..f31fafc --- /dev/null +++ b/ofx_processor/downloaders/lcl.py @@ -0,0 +1,82 @@ +import time +from pathlib import Path + +import click +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.select import Select + +from ofx_processor.utils.config import ( + get_config, + get_config_file_name, + handle_config_file_error, +) + + +class LclDownloader: + def __init__(self, download_folder: Path = None): + self.config = get_config("lcl") + if not self.config.bank_identifier or not self.config.bank_password: + handle_config_file_error( + get_config_file_name(), "Missing credentials in config file" + ) + + if not download_folder: + download_folder = Path.home() / "Downloads" + self.download_folder = download_folder.resolve() + options = webdriver.FirefoxOptions() + options.headless = True + options.set_preference("browser.download.dir", str(self.download_folder)) + options.set_preference( + "browser.helperApps.neverAsk.saveToDisk", "application/x-ofx" + ) + self.selenium = webdriver.Firefox(options=options) + self.selenium.implicitly_wait(10) + + def download(self) -> str: + selenium = self.selenium + + click.secho("Logging in to LCL...", fg="blue") + selenium.get("https://monespace.lcl.fr/connexion") + login_input = selenium.find_element(By.ID, "identifier") + login_input.send_keys(self.config.bank_identifier) + self._click(By.CLASS_NAME, "app-cta-button") + for char in self.config.bank_password: + self._click(By.CSS_SELECTOR, f".pad-button[value='{char}']") + self._click(By.CLASS_NAME, "app-cta-button") + click.secho("Logged in!", fg="green") + + click.secho("Navigating through archives...", fg="blue") + self._click(By.ID, "linkSynthese") + self._click(By.CLASS_NAME, "picDl") + self._select(By.ID, "change", index=1) + self._select(By.ID, "DS", index=20) + self._click(By.ID, "MON04") + self._click(By.ID, "Valider") + click.secho("Found it!", fg="green") + selenium.get("about:downloads") + return self._get_last_download_file_name() + + def _click(self, by: By, value: str): + self.selenium.find_element(by, value).click() + + def _select(self, by: By, value: str, index: int): + Select(self.selenium.find_element(by, value)).select_by_index(index) + + def _get_last_download_file_name(self, wait_seconds: int = 30): + end_time = time.time() + wait_seconds + while time.time() < end_time: + try: + file_name = self.selenium.execute_script( + "return document.querySelector('#contentAreaDownloadsView .downloadMainArea .downloadContainer description:nth-of-type(1)').value" + ) + if file_name: + return self.download_folder / file_name + except: + pass + time.sleep(1) + + +if __name__ == "__main__": + filename = LclDownloader().download() + print(filename) diff --git a/ofx_processor/main.py b/ofx_processor/main.py index ebf620f..356d839 100644 --- a/ofx_processor/main.py +++ b/ofx_processor/main.py @@ -1,6 +1,7 @@ import click -from ofx_processor.utils import ynab +from ofx_processor import downloaders +from ofx_processor.utils import config from ofx_processor.utils.utils import OrderedGroup, discover_processors CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @@ -15,7 +16,7 @@ def cli(): """ -cli.add_command(ynab.config, name="config") +cli.add_command(config.config, name="config") discover_processors(cli) if __name__ == "__main__": diff --git a/ofx_processor/processors/lcl.py b/ofx_processor/processors/lcl.py index df7ffc8..da2b095 100644 --- a/ofx_processor/processors/lcl.py +++ b/ofx_processor/processors/lcl.py @@ -4,6 +4,7 @@ from datetime import datetime import click import dateparser +from ofx_processor.downloaders import LclDownloader from ofx_processor.utils.base_ofx import OfxBaseLine, OfxBaseProcessor @@ -79,6 +80,14 @@ class LclProcessor(OfxBaseProcessor): return transactions -def main(filename, keep): +def main(filename, keep, download): """Import LCL bank statement (OFX file).""" + if download: + if filename: + click.secho( + "You can't specify a file name with auto download. " + "Specified file name will be ignored.", + fg="yellow", + ) + filename = LclDownloader().download() LclProcessor(filename).push_to_ynab(keep) diff --git a/ofx_processor/utils/base_processor.py b/ofx_processor/utils/base_processor.py index 942cdbb..1d1a652 100644 --- a/ofx_processor/utils/base_processor.py +++ b/ofx_processor/utils/base_processor.py @@ -47,7 +47,7 @@ class BaseProcessor: account_name = None def __init__(self, filename): - self.filename = filename + self.filename = str(filename) self.iterable = self.parse_file() self.transaction_ids = defaultdict(int) diff --git a/ofx_processor/utils/config.py b/ofx_processor/utils/config.py new file mode 100644 index 0000000..df953a5 --- /dev/null +++ b/ofx_processor/utils/config.py @@ -0,0 +1,93 @@ +import configparser +import os +import sys +from dataclasses import dataclass +from typing import Optional + +import click + +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": "", + "budget": "", + } + default_config["bpvf"] = {"account": ""} + default_config["revolut"] = {"account": ""} + default_config["ce"] = {"account": ""} + default_config["lcl"] = { + "account": "", + "bank_identifier": "login", + "bank_password": "password", + } + + return default_config + + +def get_config_file_name(): + config_file = os.path.join(DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME) + return config_file + + +@click.group() +def config(): + """Manage configuration.""" + + +@config.command("edit") +def edit_config(): + config_file = get_config_file_name() + click.edit(filename=config_file) + + +@dataclass(frozen=True) +class Config: + account: str + budget_id: str + token: str + bank_identifier: Optional[str] = None + bank_password: Optional[str] = None + + +def get_config(account: str) -> Config: + config = configparser.ConfigParser() + config_file = get_config_file_name() + + 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) + + try: + config.read(config_file) + except configparser.Error as e: + return handle_config_file_error(config_file, e) + + try: + section = config[account] + budget_id = section["budget"] + token = section["token"] + account = section["account"] + bank_identifier = section.get("bank_identifier") + bank_password = section.get("bank_password") + except KeyError as e: + return handle_config_file_error(config_file, e) + + return Config(account, budget_id, token, bank_identifier, bank_password) + + +def handle_config_file_error(config_file, e): + click.secho(f"Error while parsing config file: {str(e)}", fg="red", bold=True) + click.secho("Opening the file...") + click.pause() + click.edit(filename=config_file) + click.secho("Exiting...", fg="red", bold=True) + sys.exit(1) diff --git a/ofx_processor/utils/utils.py b/ofx_processor/utils/utils.py index b928617..bd385f5 100644 --- a/ofx_processor/utils/utils.py +++ b/ofx_processor/utils/utils.py @@ -1,11 +1,34 @@ import collections import importlib +import inspect import pkgutil import click from ofx_processor import processors +ARG_TO_OPTION = { + "keep": click.option( + "--keep/--no-keep", + help="Keep the file after processing it.", + default=False, + show_default=True, + ), + "download": click.option( + "--download/--no-download", + help="Download the file automatically.", + default=False, + show_default=True, + ), + "filename": click.option( + "-f", + "--filename", + help="Use specified file.", + default="", + show_default=True, + ), +} + def discover_processors(cli: click.Group): """ @@ -41,13 +64,10 @@ def discover_processors(cli: click.Group): # Apply default decorators method = getattr(module, "main") - method = click.option( - "--keep/--no-keep", - help="Keep the file after processing it.", - default=False, - show_default=True, - )(method) - method = click.argument("filename")(method) + method_args = inspect.getfullargspec(method).args + for arg in method_args: + if arg in ARG_TO_OPTION: + method = ARG_TO_OPTION[arg](method) method = click.command(cls.command_name)(method) cli.add_command(method) diff --git a/ofx_processor/utils/ynab.py b/ofx_processor/utils/ynab.py index a17d570..cd23243 100644 --- a/ofx_processor/utils/ynab.py +++ b/ofx_processor/utils/ynab.py @@ -1,82 +1,29 @@ -import configparser -import os -import sys - import click import requests +from ofx_processor.utils.config import get_config + 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": "", - "budget": "", - } - default_config["bpvf"] = {"account": ""} - default_config["revolut"] = {"account": ""} - default_config["ce"] = {"account": ""} - - return default_config - - -def get_config_file_name(): - config_file = os.path.join(DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME) - return config_file - - -@click.group() -def config(): - """Manage configuration.""" - - -@config.command("edit") -def edit_config(): - config_file = get_config_file_name() - click.edit(filename=config_file) def push_transactions(transactions, account): if not transactions: click.secho("No transaction, nothing to do.", fg="yellow") return - config = configparser.ConfigParser() - config_file = get_config_file_name() - 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 = get_config(account) - try: - config.read(config_file) - except configparser.Error as e: - return handle_config_file_error(config_file, e) - - try: - section = config[account] - budget_id = section["budget"] - token = section["token"] - account = section["account"] - except KeyError as e: - return handle_config_file_error(config_file, e) - - url = f"{BASE_URL}/budgets/{budget_id}/transactions" + url = f"{BASE_URL}/budgets/{config.budget_id}/transactions" for transaction in transactions: - transaction["account_id"] = account + transaction["account_id"] = config.account transaction["cleared"] = "cleared" data = {"transactions": transactions} - headers = {"Authorization": f"Bearer {token}"} + headers = {"Authorization": f"Bearer {config.token}"} res = requests.post(url, json=data, headers=headers) - res.raise_for_status() + if res.status_code >= 400: + click.secho(f"Error pushing transactions: {res.text}", fg="red") + return data = res.json()["data"] created = set() @@ -95,12 +42,3 @@ def push_transactions(transactions, account): click.secho( f"{len(duplicates)} transactions ignored (duplicates).", fg="yellow" ) - - -def handle_config_file_error(config_file, e): - click.secho(f"Error while parsing config file: {str(e)}", fg="red", bold=True) - click.secho("Opening the file...") - click.pause() - click.edit(filename=config_file) - click.secho("Exiting...", fg="red", bold=True) - sys.exit(1) diff --git a/poetry.lock b/poetry.lock index e11ba80..4ab12c1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,11 @@ +[[package]] +name = "async-generator" +version = "1.10" +description = "Async generators and context managers for Python 3.5+" +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "atomicwrites" version = "1.4.0" @@ -10,7 +18,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "attrs" version = "21.2.0" description = "Classes Without Boilerplate" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -51,6 +59,17 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "cffi" +version = "1.15.0" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + [[package]] name = "cfgv" version = "3.3.1" @@ -103,6 +122,25 @@ tomli = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["tomli"] +[[package]] +name = "cryptography" +version = "35.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools_rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + [[package]] name = "dateparser" version = "1.1.0" @@ -142,6 +180,14 @@ python-versions = ">=3.6" docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] +[[package]] +name = "h11" +version = "0.12.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "identify" version = "2.3.7" @@ -193,6 +239,17 @@ category = "main" optional = false python-versions = ">=3.8" +[[package]] +name = "outcome" +version = "1.1.0" +description = "Capture the outcome of Python function calls." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +attrs = ">=19.2.0" + [[package]] name = "packaging" version = "21.3" @@ -252,6 +309,30 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyopenssl" +version = "21.0.0" +description = "Python wrapper module around the OpenSSL library" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" + +[package.dependencies] +cryptography = ">=3.3" +six = ">=1.5.2" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +test = ["flaky", "pretend", "pytest (>=3.0.1)"] + [[package]] name = "pyparsing" version = "3.0.6" @@ -364,6 +445,19 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +[[package]] +name = "selenium" +version = "4.0.0" +description = "" +category = "main" +optional = false +python-versions = "~=3.7" + +[package.dependencies] +trio = ">=0.17,<1.0" +trio-websocket = ">=0.9,<1.0" +urllib3 = {version = ">=1.26,<2.0", extras = ["secure"]} + [[package]] name = "six" version = "1.16.0" @@ -372,6 +466,22 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "toml" version = "0.10.2" @@ -410,6 +520,36 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2, docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)", "pathlib2 (>=2.3.3)"] +[[package]] +name = "trio" +version = "0.19.0" +description = "A friendly Python library for async concurrency and I/O" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +async-generator = ">=1.9" +attrs = ">=19.2.0" +cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} +idna = "*" +outcome = "*" +sniffio = "*" +sortedcontainers = "*" + +[[package]] +name = "trio-websocket" +version = "0.9.2" +description = "WebSocket library for Trio" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +async-generator = ">=1.10" +trio = ">=0.11" +wsproto = ">=0.14" + [[package]] name = "tzdata" version = "2021.5" @@ -443,6 +583,12 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +[package.dependencies] +certifi = {version = "*", optional = true, markers = "extra == \"secure\""} +cryptography = {version = ">=1.3.4", optional = true, markers = "extra == \"secure\""} +idna = {version = ">=2.0.0", optional = true, markers = "extra == \"secure\""} +pyOpenSSL = {version = ">=0.14", optional = true, markers = "extra == \"secure\""} + [package.extras] brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] @@ -467,12 +613,27 @@ six = ">=1.9.0,<2" docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +[[package]] +name = "wsproto" +version = "1.0.0" +description = "WebSockets state-machine based protocol implementation" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +h11 = ">=0.9.0,<1" + [metadata] lock-version = "1.1" python-versions = ">=3.8,<4" -content-hash = "a841839780c92b0bfb8e131978a42fa9e9b7b75e27e7c8c3c9bc30e75ef17b96" +content-hash = "daea4d42f45c7dbccd934234140164a94ae8fa803f74b732b069026601b46a85" [metadata.files] +async-generator = [ + {file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"}, + {file = "async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"}, +] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, @@ -507,6 +668,58 @@ certifi = [ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, ] +cffi = [ + {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, + {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, + {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, + {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, + {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, + {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, + {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, + {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, + {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, + {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, + {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, + {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, + {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, + {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, + {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, + {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, + {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, +] cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, @@ -572,6 +785,28 @@ coverage = [ {file = "coverage-6.1.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:eab14fdd410500dae50fd14ccc332e65543e7b39f6fc076fe90603a0e5d2f929"}, {file = "coverage-6.1.2.tar.gz", hash = "sha256:d9a635114b88c0ab462e0355472d00a180a5fbfd8511e7f18e4ac32652e7d972"}, ] +cryptography = [ + {file = "cryptography-35.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9"}, + {file = "cryptography-35.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:ced40344e811d6abba00295ced98c01aecf0c2de39481792d87af4fa58b7b4d6"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:54b2605e5475944e2213258e0ab8696f4f357a31371e538ef21e8d61c843c28d"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7b7ceeff114c31f285528ba8b390d3e9cfa2da17b56f11d366769a807f17cbaa"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d69645f535f4b2c722cfb07a8eab916265545b3475fdb34e0be2f4ee8b0b15e"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2d0e0acc20ede0f06ef7aa58546eee96d2592c00f450c9acb89c5879b61992"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:07bb7fbfb5de0980590ddfc7f13081520def06dc9ed214000ad4372fb4e3c7f6"}, + {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7eba2cebca600a7806b893cb1d541a6e910afa87e97acf2021a22b32da1df52d"}, + {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:18d90f4711bf63e2fb21e8c8e51ed8189438e6b35a6d996201ebd98a26abbbe6"}, + {file = "cryptography-35.0.0-cp36-abi3-win32.whl", hash = "sha256:c10c797ac89c746e488d2ee92bd4abd593615694ee17b2500578b63cad6b93a8"}, + {file = "cryptography-35.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:7075b304cd567694dc692ffc9747f3e9cb393cc4aa4fb7b9f3abd6f5c4e43588"}, + {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a688ebcd08250eab5bb5bca318cc05a8c66de5e4171a65ca51db6bd753ff8953"}, + {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99915d6ab265c22873f1b4d6ea5ef462ef797b4140be4c9d8b179915e0985c6"}, + {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:928185a6d1ccdb816e883f56ebe92e975a262d31cc536429041921f8cb5a62fd"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ebeddd119f526bcf323a89f853afb12e225902a24d29b55fe18dd6fcb2838a76"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22a38e96118a4ce3b97509443feace1d1011d0571fae81fc3ad35f25ba3ea999"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb80e8a1f91e4b7ef8b33041591e6d89b2b8e122d787e87eeb2b08da71bb16ad"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:abb5a361d2585bb95012a19ed9b2c8f412c5d723a9836418fab7aaa0243e67d2"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1ed82abf16df40a60942a8c211251ae72858b25b7421ce2497c2eb7a1cee817c"}, + {file = "cryptography-35.0.0.tar.gz", hash = "sha256:9933f28f70d0517686bd7de36166dda42094eac49415459d9bdf5e7df3e0086d"}, +] dateparser = [ {file = "dateparser-1.1.0-py2.py3-none-any.whl", hash = "sha256:fec344db1f73d005182e214c0ff27313c748bbe0c1638ce9d48a809ddfdab2a0"}, {file = "dateparser-1.1.0.tar.gz", hash = "sha256:faa2b97f51f3b5ff1ba2f17be90de2b733fb6191f89b4058787473e8202f3044"}, @@ -584,6 +819,10 @@ filelock = [ {file = "filelock-3.4.0-py3-none-any.whl", hash = "sha256:2e139a228bcf56dd8b2274a65174d005c4a6b68540ee0bdbb92c76f43f29f7e8"}, {file = "filelock-3.4.0.tar.gz", hash = "sha256:93d512b32a23baf4cac44ffd72ccf70732aeff7b8050fcaf6d3ec406d954baf4"}, ] +h11 = [ + {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, + {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, +] identify = [ {file = "identify-2.3.7-py2.py3-none-any.whl", hash = "sha256:12f204544d1b718d5be2c82d95d30ff163614d7e15ae180bfbe923614a5f9a9f"}, {file = "identify-2.3.7.tar.gz", hash = "sha256:eda2c9ad2d53fcc1ff110fc9816206a9ec236a5cc8469f815fc8d8193d58363a"}, @@ -608,6 +847,10 @@ nodeenv = [ ofxtools = [ {file = "ofxtools-0.9.4.tar.gz", hash = "sha256:e27f6c6f7ca2f8b195ee7fa14384760e2ea521d89501e6652e900aa4f92bff81"}, ] +outcome = [ + {file = "outcome-1.1.0-py2.py3-none-any.whl", hash = "sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958"}, + {file = "outcome-1.1.0.tar.gz", hash = "sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967"}, +] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -628,6 +871,14 @@ py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] +pyopenssl = [ + {file = "pyOpenSSL-21.0.0-py2.py3-none-any.whl", hash = "sha256:8935bd4920ab9abfebb07c41a4f58296407ed77f04bd1a92914044b848ba1ed6"}, + {file = "pyOpenSSL-21.0.0.tar.gz", hash = "sha256:5e2d8c5e46d0d865ae933bef5230090bdaf5506281e9eec60fa250ee80600cb3"}, +] pyparsing = [ {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, @@ -742,10 +993,21 @@ requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] +selenium = [ + {file = "selenium-4.0.0-py3-none-any.whl", hash = "sha256:c942b166a21ce9c9065ad249b54059e926d39f9000167b5ca0fa4950d2ef9a82"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +sniffio = [ + {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, + {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, +] +sortedcontainers = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -758,6 +1020,14 @@ tox = [ {file = "tox-3.24.4-py2.py3-none-any.whl", hash = "sha256:5e274227a53dc9ef856767c21867377ba395992549f02ce55eb549f9fb9a8d10"}, {file = "tox-3.24.4.tar.gz", hash = "sha256:c30b57fa2477f1fb7c36aa1d83292d5c2336cd0018119e1b1c17340e2c2708ca"}, ] +trio = [ + {file = "trio-0.19.0-py3-none-any.whl", hash = "sha256:c27c231e66336183c484fbfe080fa6cc954149366c15dc21db8b7290081ec7b8"}, + {file = "trio-0.19.0.tar.gz", hash = "sha256:895e318e5ec5e8cea9f60b473b6edb95b215e82d99556a03eb2d20c5e027efe1"}, +] +trio-websocket = [ + {file = "trio-websocket-0.9.2.tar.gz", hash = "sha256:a3d34de8fac26023eee701ed1e7bf4da9a8326b61a62934ec9e53b64970fd8fe"}, + {file = "trio_websocket-0.9.2-py3-none-any.whl", hash = "sha256:5b558f6e83cc20a37c3b61202476c5295d1addf57bd65543364e0337e37ed2bc"}, +] tzdata = [ {file = "tzdata-2021.5-py2.py3-none-any.whl", hash = "sha256:3eee491e22ebfe1e5cfcc97a4137cd70f092ce59144d81f8924a844de05ba8f5"}, {file = "tzdata-2021.5.tar.gz", hash = "sha256:68dbe41afd01b867894bbdfd54fa03f468cfa4f0086bfb4adcd8de8f24f3ee21"}, @@ -774,3 +1044,7 @@ virtualenv = [ {file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"}, {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"}, ] +wsproto = [ + {file = "wsproto-1.0.0-py3-none-any.whl", hash = "sha256:d8345d1808dd599b5ffb352c25a367adb6157e664e140dbecba3f9bc007edb9f"}, + {file = "wsproto-1.0.0.tar.gz", hash = "sha256:868776f8456997ad0d9720f7322b746bbe9193751b5b290b7f924659377c8c38"}, +] diff --git a/pyproject.toml b/pyproject.toml index 41e2b79..a4e415b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ ofxtools = "^0.9.4" click = "^8.0.3" dateparser = "^1.1.0" requests = "^2.24.0" +selenium = "^4.0.0" [tool.poetry.dev-dependencies] pytest = "^6.0.1" diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index 9741f96..7789070 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -7,8 +7,9 @@ from unittest.mock import call from click.testing import CliRunner +import ofx_processor.utils.config from ofx_processor.utils import utils, ynab -from ofx_processor.utils.ynab import config +from ofx_processor.utils.config import config class UtilsTestCase(unittest.TestCase): @@ -38,13 +39,14 @@ class ConfigTestCase(unittest.TestCase): # This is run at import time and the cli module is already imported before this test # so we need to re-run the add_command to make it available. - cli.add_command(ynab.config, name="config") + cli.add_command(ofx_processor.utils.config.config, name="config") utils.discover_processors(cli) self.cli = cli @mock.patch("click.edit") @mock.patch( - "ofx_processor.utils.ynab.DEFAULT_CONFIG_DIR", os.path.join("tests", "samples") + "ofx_processor.utils.config.DEFAULT_CONFIG_DIR", + os.path.join("tests", "samples"), ) def test_config_edit(self, edit): runner = CliRunner() @@ -55,11 +57,12 @@ class ConfigTestCase(unittest.TestCase): @mock.patch("click.edit") @mock.patch( - "ofx_processor.utils.ynab.DEFAULT_CONFIG_FILENAME", + "ofx_processor.utils.config.DEFAULT_CONFIG_FILENAME", "config_broken_duplicate_key.ini", ) @mock.patch( - "ofx_processor.utils.ynab.DEFAULT_CONFIG_DIR", os.path.join("tests", "samples") + "ofx_processor.utils.config.DEFAULT_CONFIG_DIR", + os.path.join("tests", "samples"), ) def test_broken_config_file(self, edit): broken_files = [ @@ -70,12 +73,12 @@ class ConfigTestCase(unittest.TestCase): "config_broken_missing_token.ini", ] for name in broken_files: - with mock.patch("ofx_processor.utils.ynab.DEFAULT_CONFIG_FILENAME", name): + with mock.patch("ofx_processor.utils.config.DEFAULT_CONFIG_FILENAME", name): runner = CliRunner() result = runner.invoke( - self.cli, ["revolut", "tests/samples/revolut.csv", "--keep"] + self.cli, ["revolut", "-f", "tests/samples/revolut.csv", "--keep"] ) - expected_filename = ynab.get_config_file_name() + expected_filename = ofx_processor.utils.config.get_config_file_name() self.assertIn( "Error while parsing config file", result.output, @@ -83,36 +86,43 @@ class ConfigTestCase(unittest.TestCase): ) edit.assert_called_with(filename=expected_filename) - @mock.patch("ofx_processor.utils.ynab.DEFAULT_CONFIG_FILENAME", "file.ini") + @mock.patch("ofx_processor.utils.config.DEFAULT_CONFIG_FILENAME", "file.ini") @mock.patch( - "ofx_processor.utils.ynab.DEFAULT_CONFIG_DIR", + "ofx_processor.utils.config.DEFAULT_CONFIG_DIR", os.path.join("some", "config", "folder"), ) def test_get_config_file_name(self): expected = os.path.join("some", "config", "folder", "file.ini") - self.assertEqual(ynab.get_config_file_name(), expected) + self.assertEqual(ofx_processor.utils.config.get_config_file_name(), expected) + @mock.patch("requests.post") @mock.patch("click.edit") @mock.patch("os.makedirs") - @mock.patch("ofx_processor.utils.ynab.DEFAULT_CONFIG_FILENAME", "notfound.ini") + @mock.patch("ofx_processor.utils.config.DEFAULT_CONFIG_FILENAME", "notfound.ini") @mock.patch( - "ofx_processor.utils.ynab.DEFAULT_CONFIG_DIR", os.path.join("tests", "samples") + "ofx_processor.utils.config.DEFAULT_CONFIG_DIR", + os.path.join("tests", "samples"), ) - def test_missing_config_file(self, makedirs, edit): + def test_missing_config_file(self, makedirs, edit, *args): runner = CliRunner() - with mock.patch("ofx_processor.utils.ynab.open", mock.mock_open()) as mock_file: + with mock.patch( + "ofx_processor.utils.config.open", mock.mock_open() + ) as mock_file: result = runner.invoke( - self.cli, ["revolut", "tests/samples/revolut.csv", "--keep"] + self.cli, ["revolut", "-f", "tests/samples/revolut.csv", "--keep"] ) mock_file.assert_called_once() - makedirs.assert_called_once_with(ynab.DEFAULT_CONFIG_DIR, exist_ok=True) + makedirs.assert_called_once_with( + ofx_processor.utils.config.DEFAULT_CONFIG_DIR, exist_ok=True + ) self.assertIn("Editing config file", result.output) - expected_filename = ynab.get_config_file_name() + expected_filename = ofx_processor.utils.config.get_config_file_name() edit.assert_called_once_with(filename=expected_filename) + # post.assert_not_called() @mock.patch( - "ofx_processor.utils.ynab.DEFAULT_CONFIG_DIR", os.path.join("tests", "samples") + "ofx_processor.utils.config.DEFAULT_CONFIG_DIR", os.path.join("tests", "samples") ) class DataTestCase(unittest.TestCase): def setUp(self): @@ -120,12 +130,13 @@ class DataTestCase(unittest.TestCase): # This is run at import time and the cli module is already imported before this test # so we need to re-run the add_command to make it available. - cli.add_command(ynab.config, name="config") + cli.add_command(ofx_processor.utils.config.config, name="config") utils.discover_processors(cli) self.cli = cli @mock.patch("requests.post") - def test_file_is_deleted_by_default(self, *args): + def test_file_is_deleted_by_default(self, post): + post.return_value.status_code = 201 runner = CliRunner() revolut_csv = "tests/samples/revolut.csv" backup_file = "tests/samples/backup.csv" @@ -135,7 +146,7 @@ class DataTestCase(unittest.TestCase): try: self.assertTrue(os.path.isfile(revolut_csv)) - result = runner.invoke(self.cli, ["revolut", revolut_csv]) + result = runner.invoke(self.cli, ["revolut", "-f", revolut_csv]) self.assertEqual(result.exit_code, 0) self.assertFalse(os.path.isfile(revolut_csv)) finally: @@ -144,7 +155,8 @@ class DataTestCase(unittest.TestCase): os.unlink(backup_file) @mock.patch("requests.post") - def test_file_is_kept_with_option(self, *args): + def test_file_is_kept_with_option(self, post): + post.return_value.status_code = 201 runner = CliRunner() revolut_csv = "tests/samples/revolut.csv" backup_file = "tests/samples/backup.csv" @@ -154,7 +166,9 @@ class DataTestCase(unittest.TestCase): try: self.assertTrue(os.path.isfile(revolut_csv)) - result = runner.invoke(self.cli, ["revolut", revolut_csv, "--keep"]) + result = runner.invoke( + self.cli, ["revolut", "--filename", revolut_csv, "--keep"] + ) self.assertEqual(result.exit_code, 0) self.assertTrue(os.path.isfile(revolut_csv)) finally: @@ -164,6 +178,7 @@ class DataTestCase(unittest.TestCase): @mock.patch("requests.post") def test_revolut_sends_data_only_created(self, post): + post.return_value.status_code = 201 post.return_value.json.return_value = { "data": { "transactions": [ @@ -198,7 +213,7 @@ class DataTestCase(unittest.TestCase): runner = CliRunner() result = runner.invoke( - self.cli, ["revolut", "tests/samples/revolut.csv", "--keep"] + self.cli, ["revolut", "-f", "tests/samples/revolut.csv", "--keep"] ) self.assertEqual(result.exit_code, 0) @@ -208,6 +223,7 @@ class DataTestCase(unittest.TestCase): @mock.patch("requests.post") def test_revolut_sends_data_some_created_some_duplicates(self, post): + post.return_value.status_code = 201 post.return_value.json.return_value = { "data": { "transactions": [ @@ -243,7 +259,7 @@ class DataTestCase(unittest.TestCase): runner = CliRunner() result = runner.invoke( - self.cli, ["revolut", "tests/samples/revolut.csv", "--keep"] + self.cli, ["revolut", "-f", "tests/samples/revolut.csv", "--keep"] ) self.assertEqual(result.exit_code, 0) @@ -253,6 +269,7 @@ class DataTestCase(unittest.TestCase): @mock.patch("requests.post") def test_revolut_sends_data_only_duplicates(self, post): + post.return_value.status_code = 201 post.return_value.json.return_value = { "data": { "transactions": [], @@ -273,7 +290,7 @@ class DataTestCase(unittest.TestCase): runner = CliRunner() result = runner.invoke( - self.cli, ["revolut", "tests/samples/revolut.csv", "--keep"] + self.cli, ["revolut", "-f", "tests/samples/revolut.csv", "--keep"] ) self.assertEqual(result.exit_code, 0) @@ -283,6 +300,7 @@ class DataTestCase(unittest.TestCase): @mock.patch("requests.post") def test_bpvf_sends_to_ynab(self, post): + post.return_value.status_code = 201 with open("tests/samples/bpvf_transactions.json", encoding="utf-8") as f: expected_data = json.load(f) @@ -290,7 +308,7 @@ class DataTestCase(unittest.TestCase): expected_url = f"{ynab.BASE_URL}/budgets//transactions" runner = CliRunner() - runner.invoke(self.cli, ["bpvf", "tests/samples/bpvf.ofx", "--keep"]) + runner.invoke(self.cli, ["bpvf", "-f", "tests/samples/bpvf.ofx", "--keep"]) post.assert_called_once_with( expected_url, json=expected_data, headers=expected_headers @@ -298,6 +316,7 @@ class DataTestCase(unittest.TestCase): @mock.patch("requests.post") def test_ce_sends_to_ynab(self, post): + post.return_value.status_code = 201 with open("tests/samples/ce_transactions.json", encoding="utf-8") as f: expected_data = json.load(f) @@ -305,7 +324,7 @@ class DataTestCase(unittest.TestCase): expected_url = f"{ynab.BASE_URL}/budgets//transactions" runner = CliRunner() - runner.invoke(self.cli, ["ce", "tests/samples/ce.ofx", "--keep"]) + runner.invoke(self.cli, ["ce", "-f", "tests/samples/ce.ofx", "--keep"]) post.assert_called_once_with( expected_url, json=expected_data, headers=expected_headers @@ -313,6 +332,7 @@ class DataTestCase(unittest.TestCase): @mock.patch("requests.post") def test_lcl_sends_to_ynab(self, post): + post.return_value.status_code = 201 with open("tests/samples/lcl_transactions.json", encoding="utf-8") as f: expected_data = json.load(f) @@ -320,7 +340,7 @@ class DataTestCase(unittest.TestCase): expected_url = f"{ynab.BASE_URL}/budgets//transactions" runner = CliRunner() - runner.invoke(self.cli, ["lcl", "tests/samples/lcl.ofx", "--keep"]) + runner.invoke(self.cli, ["lcl", "-f", "tests/samples/lcl.ofx", "--keep"]) post.assert_called_once_with( expected_url, json=expected_data, headers=expected_headers diff --git a/tests/test_ynab_integration.py b/tests/test_ynab_integration.py index 594a814..0587c6f 100644 --- a/tests/test_ynab_integration.py +++ b/tests/test_ynab_integration.py @@ -2,13 +2,15 @@ import json import unittest from unittest import mock +import ofx_processor.utils.config from ofx_processor.utils import ynab class YNABIntegrationTestCase(unittest.TestCase): @mock.patch("requests.post") def test_data_sent_to_ynab(self, post): - ynab.DEFAULT_CONFIG_DIR = "tests/samples" + post.return_value.status_code = 201 + ofx_processor.utils.config.DEFAULT_CONFIG_DIR = "tests/samples" with open("tests/samples/revolut_expected.json", encoding="utf-8") as f: transactions = json.load(f)