forked from gaugendre/ofx-processor
Add LCL automatic file download
This commit is contained in:
parent
08c710b178
commit
96fbeb257b
13 changed files with 557 additions and 115 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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
|
||||
|
|
1
ofx_processor/downloaders/__init__.py
Normal file
1
ofx_processor/downloaders/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from .lcl import LclDownloader
|
82
ofx_processor/downloaders/lcl.py
Normal file
82
ofx_processor/downloaders/lcl.py
Normal file
|
@ -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)
|
|
@ -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__":
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
93
ofx_processor/utils/config.py
Normal file
93
ofx_processor/utils/config.py
Normal file
|
@ -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": "<YOUR API TOKEN>",
|
||||
"budget": "<YOUR BUDGET ID>",
|
||||
}
|
||||
default_config["bpvf"] = {"account": "<YOUR ACCOUNT ID>"}
|
||||
default_config["revolut"] = {"account": "<YOUR ACCOUNT ID>"}
|
||||
default_config["ce"] = {"account": "<YOUR ACCOUNT ID>"}
|
||||
default_config["lcl"] = {
|
||||
"account": "<YOUR ACCOUNT ID>",
|
||||
"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)
|
|
@ -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)
|
||||
|
|
|
@ -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": "<YOUR API TOKEN>",
|
||||
"budget": "<YOUR BUDGET ID>",
|
||||
}
|
||||
default_config["bpvf"] = {"account": "<YOUR ACCOUNT ID>"}
|
||||
default_config["revolut"] = {"account": "<YOUR ACCOUNT ID>"}
|
||||
default_config["ce"] = {"account": "<YOUR ACCOUNT ID>"}
|
||||
|
||||
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)
|
||||
|
|
278
poetry.lock
generated
278
poetry.lock
generated
|
@ -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"},
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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/<YOUR BUDGET ID>/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/<YOUR BUDGET ID>/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/<YOUR BUDGET ID>/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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue