From 6d60d99ddfbf5dc739820ad474eef13df9c5be88 Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Tue, 8 Jul 2025 21:44:48 -0700 Subject: [PATCH] Remove all OFX Home code `https://www.ofxhome.com` has been down for a while. AFAIK, it's not coming back. ofxtools already has a bunch of useful data from OFX Home imported into `ofxtools/config/fi.cfg`. However, people cannot benefit from it because ofxtools tries to grab more up to date data from ofxhome.com, which crashes. See https://github.com/csingley/ofxtools/issues/193 for details. This change simply removes all ofxhome code, allowing at least some things to work again. This completes https://github.com/csingley/ofxtools/issues/193 --- docs/client.rst | 25 ----- docs/resources.rst | 1 - ofxtools/config/ofxget_example.cfg | 8 -- ofxtools/ofxhome.py | 168 ---------------------------- ofxtools/scripts/ofxget.py | 45 ++------ ofxtools/scripts/update_fi_cfg.py | 139 ----------------------- tests/test_ofxget.py | 173 ----------------------------- tests/test_ofxhome.py | 102 ----------------- 8 files changed, 7 insertions(+), 654 deletions(-) delete mode 100644 ofxtools/ofxhome.py delete mode 100644 ofxtools/scripts/update_fi_cfg.py delete mode 100644 tests/test_ofxhome.py diff --git a/docs/client.rst b/docs/client.rst index b22d6996..1b30d33d 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -164,13 +164,6 @@ its nickname instead: $ ofxget prof amex -Or, if the server is known to OFX Home, then you can just use its database -ID (the end part of its `institution page on OFX Home`_): - -.. code-block:: bash - - $ ofxget prof --ofxhome 424 - Any of these work just fine, dumping a load of markup on the screen telling us what OFX services are available and some parameters for accessing them. @@ -192,12 +185,6 @@ provide a server nickname. $ ofxget prof myfi --write --org AMEX --fid 3101 --url https://online.americanexpress.com/myca/ofxdl/desktop/desktopDownload.do\?request_type\=nl_ofxdownload -If your server is up on OFX Home, this works as well: - -.. code-block:: bash - - ofxget prof myfi --ofxhome 424 --write - It's also easy to write a configuration file manually in a text editor - it's just the command line options in simple INI format, with a server nicknames as section headers. You can find a sample at @@ -232,16 +219,6 @@ Our configuration file will look like this: org: AMEX fid: 3101 -Alternatively, since AmEx has working parameters listed on OFX Home, you could -just use the OFX Home API to look them up for each request. Using the OFX Home -database id (at the end of the webpage URL), the config looks like this: - -.. code-block:: ini - - # American Express - [amex] - ofxhome: 424 - With either configuration, we can now use the provider nickname to make our connection more conveniently: @@ -608,8 +585,6 @@ Other methods available: * ``OFXClient.request_accounts()``- ACCTINFORQ * ``OFXClient.request_tax1099()``- TAX1099RQ (still a WIP) -.. _OFX Home: http://www.ofxhome.com/ -.. _institution page on OFX Home: http://www.ofxhome.com/index.php/institution/view/424 .. _OFX Blog: https://ofxblog.wordpress.com/ .. _ABA routing number: http://routingnumber.aba.com/default1.aspx .. _getfidata.sh: https://web.archive.org/web/20070120102800/http://www.jongsma.org/gc/bankinfo/getfidata.sh.gz diff --git a/docs/resources.rst b/docs/resources.rst index 2334aa16..89eecc23 100644 --- a/docs/resources.rst +++ b/docs/resources.rst @@ -18,7 +18,6 @@ More open-source OFX code .. _OFX spec: https://financialdataexchange.org/ofx .. _Quicken data mapping guide: https://web.archive.org/web/20110908185057if_/http://fi.intuit.com/ofximplementation/dl/OFXDataMappingGuide.pdfi -.. _OFX Home: http://www.ofxhome.com/ .. _libofx: https://github.com/libofx/libofx .. _ofxparse: https://github.com/jseutter/ofxparse .. _csv2ofx: https://github.com/reubano/csv2ofx diff --git a/ofxtools/config/ofxget_example.cfg b/ofxtools/config/ofxget_example.cfg index 00213554..ab1f4403 100644 --- a/ofxtools/config/ofxget_example.cfg +++ b/ofxtools/config/ofxget_example.cfg @@ -38,14 +38,6 @@ bankid = 056073502 checking = 1234567890 moneymrkt = 1234567890 -# Since credit card accts don't have a routing #, they can be placed in -# any config section with proper url/org/fid -[amex] -# An example of referring to OFX Home for an API lookup, rather than manually -# specifying URL/ORG/FID. -ofxhome = 424 -creditcard = 111122233344556, 222333445561111 - # Brokerage accounts are specified by brokerid/investment [ameritrade] url = https://ofxs.ameritrade.com/cgi-bin/apps/OFX diff --git a/ofxtools/ofxhome.py b/ofxtools/ofxhome.py deleted file mode 100644 index f968a587..00000000 --- a/ofxtools/ofxhome.py +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env python -""" -Interface with http://ofxhome.com API -""" - - -__all__ = [ - "URL", - "VALID_DAYS", - "OFXServer", - "list_institutions", - "lookup", - "ofx_invalid", - "ssl_invalid", -] - - -# stdlib imports -from collections import OrderedDict -import datetime -import xml.etree.ElementTree as ET -from xml.sax import saxutils -import urllib -import urllib.error as urllib_error -import urllib.parse as urllib_parse -import re -from typing import Dict, NamedTuple, Optional, Union, Mapping, Match - - -URL = "http://www.ofxhome.com/api.php" -VALID_DAYS = 90 - - -FID_REGEX = re.compile(r"([^<]*)") - - -class OFXServer(NamedTuple): - """Container for an OFX Home FI record""" - - id: Optional[str] = None - name: Optional[str] = None - fid: Optional[str] = None - org: Optional[str] = None - url: Optional[str] = None - brokerid: Optional[str] = None - ofxfail: Optional[bool] = True - sslfail: Optional[bool] = True - lastofxvalidation: Optional[datetime.datetime] = None - lastsslvalidation: Optional[datetime.datetime] = None - profile: Optional[Dict[str, Union[str, bool]]] = None - - -def list_institutions() -> Mapping[str, str]: - query = _make_query(all="yes") - with urllib.request.urlopen(query) as f: - response = f.read() - - return { - fi.get("id").strip(): fi.get("name").strip() # type: ignore - for fi in ET.fromstring(response) - } - - -def lookup(id: str) -> Optional[OFXServer]: - etree = fetch_fi_xml(id) - if etree is None: - return None - - converters = { - "ofxfail": _convert_bool, - "sslfail": _convert_bool, - "lastofxvalidation": _convert_dt, - "lastsslvalidation": _convert_dt, - "profile": _convert_profile, - } - - # mypy doesn't accept NamedTuple(**kwargs); use OrderedDict as workaround - attrs = [(e.tag, converters.get(e.tag, _convert_str)(e)) for e in etree] - attrs.insert(0, ("id", etree.attrib["id"])) - return OFXServer(**OrderedDict(attrs)) # type: ignore - - -def fetch_fi_xml(id: str) -> Optional[ET.Element]: - if not id: - return None - - query = _make_query(lookup=id) - try: - with urllib.request.urlopen(query) as f: - response = f.read() - except urllib_error.URLError: - return None - - try: - etree = ET.fromstring(response) - except ET.ParseError: - # OFX Home fails to escape XML control characters for - response_ = FID_REGEX.sub(_escape_fid, response.decode()) - etree = ET.fromstring(response_) - return etree - - -def ofx_invalid(srvr: OFXServer, valid_days: Optional[int] = None) -> bool: - if srvr.ofxfail: - return True - if srvr.lastofxvalidation is None: - return True - if valid_days is None: - valid_days = VALID_DAYS - now = datetime.datetime.now() - if (now - srvr.lastofxvalidation) > datetime.timedelta(days=valid_days): - return True - - return False - - -def ssl_invalid(srvr: OFXServer, valid_days: Optional[int] = None) -> bool: - if srvr.sslfail: - return True - if srvr.lastsslvalidation is None: - return True - if valid_days is None: - valid_days = VALID_DAYS - now = datetime.datetime.now() - if (now - srvr.lastsslvalidation) > datetime.timedelta(days=valid_days): - return True - - return False - - -def _make_query(**kwargs: str) -> str: - params = urllib_parse.urlencode(kwargs) - return "{}?{}".format(URL, params) - - -def _convert_str(elem: ET.Element) -> Optional[str]: - text = elem.text - if text: - return saxutils.unescape(text).strip() - return None - - -def _convert_dt(elem: ET.Element) -> Optional[datetime.datetime]: - text = elem.text - if text: - return datetime.datetime.strptime(text, "%Y-%m-%d %H:%M:%S") - return None - - -def _convert_bool(elem: ET.Element) -> Optional[bool]: - text = elem.text - if text: - return bool(int(text)) - return None - - -def _convert_profile(elem: ET.Element) -> Dict[str, Union[str, bool]]: - def convert_maybe_bool(key: str, val: str) -> Union[str, bool]: - if key.endswith("msgset"): - return {"true": True, "false": False}[val] - return saxutils.unescape(val) - - return {k: convert_maybe_bool(k, v) for k, v in elem.attrib.items()} - - -def _escape_fid(match: Match) -> str: - fid = saxutils.escape(match.group(1)) - return "{}".format(fid) diff --git a/ofxtools/scripts/ofxget.py b/ofxtools/scripts/ofxget.py index 82adccef..919ddcdb 100644 --- a/ofxtools/scripts/ofxget.py +++ b/ofxtools/scripts/ofxget.py @@ -49,7 +49,7 @@ # local imports -from ofxtools import Client, header, Parser, utils, ofxhome, config, models +from ofxtools import Client, header, Parser, utils, config, models from ofxtools.Client import ( OFXClient, StmtRq, @@ -206,9 +206,6 @@ def add_subparser( if server: parser.add_argument("--url", help="OFX server URL") - parser.add_argument( - "--ofxhome", metavar="ID#", help="FI id# on http://www.ofxhome.com/" - ) parser.add_argument( "-w", "--write", @@ -849,7 +846,6 @@ def __init__(self, *args, **kwargs): "verbose": 0, "server": "", "url": "", - "ofxhome": "", "version": 203, "org": "", "fid": "", @@ -898,13 +894,11 @@ def __init__(self, *args, **kwargs): # "Configurable" means "will be read from / written to config file". # Subdivided into - # "Configurable server", i.e. parameters used establish an OFX connection -# (the kind of thing you'd pass to OFXClient.__init__(), which is -# how they're used by update_fi_cfg.py); and +# (the kind of thing you'd pass to OFXClient.__init__()); and # "Configurable user", i.e. auth/account parameters that are completely # user-specific and won't be shared by different users of the library. configurable_srvr = ( "url", - "ofxhome", "version", "pretty", "unclosedelements", @@ -1081,13 +1075,6 @@ def merge_config( merged: ArgsType = ChainMap(_args, user_cfg, DEFAULTS) # type: ignore # logger.debug(f"CLI args merged with user configs and defaults: {extrargs(merged)}") - # Try to perform an OFX Home lookup if: - # - it's configured from the CLI - # - it's configured in ofxget.cfg - # - we don't have a URL - if "ofxhome" in _args or "ofxhome" in user_cfg or (not merged["url"]): - merge_from_ofxhome(merged) - if not ( merged.get("url", None) or merged.get("dryrun", False) @@ -1099,7 +1086,7 @@ def merge_config( logger.error(err) msg = ( f"{err} - please provide a server nickname, " - "or configure 'url' / 'ofxhome'\n" + "or configure 'url'\n" ) print(msg) command = merged["request"] @@ -1113,33 +1100,13 @@ def merge_config( merged["server"] = None else: logger.error(err) - msg = f"{err} - please configure 'url' or 'ofxhome' for server '{server}'" + msg = f"{err} - please configure 'url' for server '{server}'" raise ValueError(msg) logger.info(f"Merged args: {extrargs(merged)}") return merged -def merge_from_ofxhome(args: ArgsType): - ofxhome_id = args["ofxhome"] - if ofxhome_id: - logger.info(f"Looking up OFX Home API for id#{ofxhome_id}") - lookup = ofxhome.lookup(ofxhome_id) - if lookup: - logger.debug(f"OFX Home lookup found {lookup}") - # Insert OFX Home lookup ahead of DEFAULTS but after - # CLI args and user configss - args.maps.insert( - -1, - { - "url": lookup.url, - "org": lookup.org, - "fid": lookup.fid, - "brokerid": lookup.brokerid, - }, - ) - - def extrargs(args: ArgsType) -> dict: """Extract non-null args""" return {k: v for k, v in args.items() if v not in NULL_ARGS} @@ -1484,6 +1451,7 @@ def list_fis(args: ArgsType) -> None: msg = f"Unknown server '{server}'" raise ValueError(msg) else: + # Note: ofxhome.com doesn't exist anymore, but we still use its IDs for name lookups. ofxhome = USERCFG[server].get("ofxhome", "") name = USERCFG["NAMES"].get(ofxhome, "") config = [" = ".join(pair) for pair in USERCFG[server].items()] @@ -1499,7 +1467,8 @@ def fi_index() -> Sequence[Tuple[str, str, str]]: names = {id_: name for id_, name in USERCFG["NAMES"].items()} cfg_default_sect = USERCFG.default_section # type: ignore servers = [ - (names.get(sct.get("ofxhome", None), ""), nick, sct.get("ofxhome", "--")) + # Note: ofxhome.com doesn't exist anymore, but we still use its IDs for name lookups. + (names.get(sct.get("ofxhome", ""), ""), nick, sct.get("ofxhome", "--")) for nick, sct in USERCFG.items() if nick not in (cfg_default_sect, "NAMES") and "url" in sct ] diff --git a/ofxtools/scripts/update_fi_cfg.py b/ofxtools/scripts/update_fi_cfg.py deleted file mode 100644 index 933efad2..00000000 --- a/ofxtools/scripts/update_fi_cfg.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 -""" -Perform an OFX profile scan for the entire OFX Home list of FIs; -update config/fi.cfg - -You probably don't want to run this script; it's for the library developers. - -It takes a long time to run and generates lots of HTTP requests, and the output -is not at all stable so it needs to be checked. -""" - - -# stdlib imports -import configparser -from configparser import ConfigParser -from xml.sax import saxutils -from typing import Mapping, Optional, ChainMap -import logging - - -# local imports -from ofxtools import ofxhome, config -from ofxtools.scripts import ofxget - - -LibraryConfig = ConfigParser() -LibraryConfig.read(ofxget.CONFIGPATH) - -if not LibraryConfig.has_section("NAMES"): - LibraryConfig["NAMES"] = {} - -# Map ofxhome_id: server_nick for all configs in library -known_servers = { - LibraryConfig[sct]["ofxhome"]: sct - for sct in LibraryConfig - if "ofxhome" in LibraryConfig[sct] -} - - -def mk_server_cfg(args: ofxget.ArgsType) -> configparser.SectionProxy: - """ - Stripped-down version of ofxget.mk_server_cfg() - """ - server = args["server"] - assert server - - if not LibraryConfig.has_section(server): - LibraryConfig[server] = {} - cfg = LibraryConfig[server] - - for opt, opt_type in ofxget.CONFIGURABLE_SRVR.items(): - if opt in args: - value = args[opt] - default_value = ofxget.DEFAULTS[opt] - if value != default_value and value not in ofxget.NULL_ARGS: - cfg[opt] = ofxget.arg2config(opt, opt_type, value) - - return cfg - - -def write_config(args: ofxget.ArgsType) -> None: - """ - Modified version of ofxget.write_config() - """ - mk_server_cfg(args) - - with open(ofxget.CONFIGPATH, "w") as f: - LibraryConfig.write(f) - - -def main(): - fis: Mapping[str, str] = ofxhome.list_institutions() - for ofxhome_id in fis.keys(): - print("Scanning {}".format(ofxhome_id)) - - lookup: Optional[ofxhome.OFXServer] = ofxhome.lookup(ofxhome_id) - if lookup is None or lookup.url is None: - continue - - url = lookup.url - org = lookup.org - fid = lookup.fid - - lookup_name = saxutils.unescape(lookup.name) - srvr_nick = known_servers.get(ofxhome_id, lookup_name) - - ofxhome_id = lookup.id - assert ofxhome_id - names = LibraryConfig["NAMES"] - if ofxhome_id not in names: - names[ofxhome_id] = lookup_name - - if ofxhome.ofx_invalid(lookup) or ofxhome.ssl_invalid(lookup): - blank_fmt = {"versions": [], "formats": {}} - scan_results = (blank_fmt, blank_fmt, {}) - else: - scan_results: ofxget.ScanResults = ofxget._scan_profile( - url=url, org=org, fid=fid, timeout=10.0 - ) - - v1, v2, _ = scan_results - if (not v2["versions"]) and (not v1["versions"]): - # If no OFX response, blank the server config - LibraryConfig[srvr_nick] = {"ofxhome": ofxhome_id} - continue - - format = ofxget._best_scan_format(scan_results) - - looked_up_data = { - "ofxhome": ofxhome_id, - "url": url, - "org": org, - "fid": fid, - "brokerid": lookup.brokerid, - } - - args = ChainMap({"server": srvr_nick}, looked_up_data, format) - write_config(args) - - -LOG_LEVELS = {0: logging.WARN, 1: logging.INFO, 2: logging.DEBUG} - - -if __name__ == "__main__": - from argparse import ArgumentParser - - argparser = ArgumentParser(description="Scan all FIs; update fi.cfg") - argparser.add_argument( - "--verbose", - "-v", - action="count", - default=0, - help="Give more output (option can be repeated)", - ) - args = argparser.parse_args() - log_level = LOG_LEVELS.get(args.verbose, logging.DEBUG) - config.configure_logging(log_level) - main() diff --git a/tests/test_ofxget.py b/tests/test_ofxget.py index 37d00cd5..7275837d 100644 --- a/tests/test_ofxget.py +++ b/tests/test_ofxget.py @@ -28,7 +28,6 @@ ) from ofxtools.utils import UTC from ofxtools.scripts import ofxget -from ofxtools.ofxhome import OFXServer # test imports import base @@ -821,7 +820,6 @@ def testMkservercfg(self): # args equal to defaults are omitted from the results predicted = { "url": "https://ofxget.test.com", - "ofxhome": "123", "org": "TEST", "fid": "321", "brokerid": "test.com", @@ -893,177 +891,6 @@ def testString2config(self): self.assertEqual(ofxget.arg2config(cfg, str, "Something"), "Something") -class MergeConfigTestCase(unittest.TestCase): - @property - def args(self): - return argparse.Namespace( - server="2big2fail", - dtstart="20070101000000", - dtend="20071231000000", - dtasof="20071231000000", - checking=None, - savings=["444"], - moneymrkt=None, - creditline=None, - creditcard=["555"], - investment=["666"], - inctran=True, - incoo=False, - incpos=True, - incbal=True, - dryrun=True, - user=None, - clientuid=None, - unclosedelements=False, - ) - - @classmethod - def setUpClass(cls): - # Monkey-patch ofxget.USERCFG - default_cfg = """ - [2big2fail] - ofxhome = 417 - version = 203 - pretty = true - fid = 44 - org = 2big2fail - """ - - user_cfg = """ - [2big2fail] - fid = 33 - user = porkypig - savings = 111 - checking = 222, 333 - creditcard = 444, 555 - """ - - cfg = ofxget.UserConfig() - cfg.read_string(default_cfg) - cfg.read_string(user_cfg) - - cls._USERCFG = ofxget.USERCFG - ofxget.USERCFG = cfg - - @classmethod - def tearDownClass(cls): - # Undo monkey patches for ofxget.USERCFG - # ofxget.UserConfig = cls._UserConfig - ofxget.USERCFG = cls._USERCFG - - def testMergeConfig(self): - args = argparse.Namespace( - server="2big2fail", user="daffyduck", creditcard=["666"] - ) - - with patch("ofxtools.ofxhome.lookup") as ofxhome_lookup: - ofxhome_lookup.return_value = OFXServer( - id="1", - name="Two Big Two Fail", - fid="22", - org="2BIG2FAIL", - url="https://ofx.test.com", - brokerid="2big2fail.com", - ) - - merged = ofxget.merge_config(args, ofxget.USERCFG) - - # None of args/usercfg/defaultcfg has the URL, - # so there should have been an OFX Home lookup - ofxhome_lookup.assert_called_once_with("417") - - # ChainMap(args, user_cfg, ofxhome_lookup, DEFAULTS) - self.assertIsInstance(merged, collections.ChainMap) - maps = merged.maps - self.assertEqual(len(maps), 4) - self.assertEqual(maps[0]["user"], "daffyduck") - self.assertEqual(maps[1]["user"], "porkypig") - self.assertEqual(maps[2]["org"], "2BIG2FAIL") - self.assertEqual(maps[3], ofxget.DEFAULTS) - - # Any arg from the the CLI should be available in the merged map. - self.assertEqual(merged["server"], "2big2fail") - - # Args passed from the CLI trump the same args from any other source. - self.assertEqual(merged["user"], "daffyduck") - - # For list-type configs, higher-priority config overrides - # lower-priority config (i.e. it's not appended). - self.assertEqual(merged["creditcard"], ["666"]) - - # Args missing from CLI fall back to user config... - self.assertEqual(merged["savings"], ["111"]) - - # ...or, failing that, fall back to library default config... - self.assertEqual(merged["org"], "2big2fail") - - # ...or, failing that, fall back to ofxhome lookup - self.assertEqual(merged["brokerid"], "2big2fail.com") - - # ...or, failing THAT, fall back to ofxget.DEFAULTS - self.assertEqual(merged["unsafe"], False) - - # User config trumps library default config and ofxhome lookup - self.assertEqual(merged["fid"], "33") - - # Library default config trumps ofxhome lookup - self.assertEqual(merged["org"], "2big2fail") - - # Library default config drumps ofxget.DEFAULTS - # Also, INI bool conversion works - self.assertEqual(merged["pretty"], True) - - # INI list chunking works - self.assertEqual(maps[1]["checking"], ["222", "333"]) - - # INI int conversion works - self.assertEqual(maps[1]["version"], 203) - - # We have proper types for all lists, even absent configuration - for lst in ( - "checking", - "savings", - "moneymrkt", - "creditline", - "creditcard", - "investment", - "years", - ): - self.assertIsInstance(merged[lst], list) - - # We have proper types for all bools, even absent configuration - for boole in ( - "dryrun", - "unsafe", - "unclosedelements", - "pretty", - "inctran", - "incbal", - "incpos", - "incoo", - "all", - "write", - ): - self.assertIsInstance(merged[boole], bool) - - # We have default empty string for all unset string configs - for string in ( - "appid", - "appver", - "bankid", - "clientuid", - "language", - "acctnum", - "recid", - ): - self.assertEqual(merged[string], "") - - def testMergeConfigUnknownFiArg(self): - args = argparse.Namespace(server="3big4fail") - with self.assertRaises(ValueError): - ofxget.merge_config(args, ofxget.USERCFG) - - ############################################################################### # PROFILE SCAN ############################################################################### diff --git a/tests/test_ofxhome.py b/tests/test_ofxhome.py deleted file mode 100644 index 34eda702..00000000 --- a/tests/test_ofxhome.py +++ /dev/null @@ -1,102 +0,0 @@ -# coding: utf-8 -""" Unit tests for ofxtools.ofxhome """ - -# stdlib imports -import unittest -from unittest.mock import patch -from io import BytesIO -from collections import OrderedDict -from datetime import datetime - -# local imports -from ofxtools import ofxhome - - -class ListInstitutionsTestCase(unittest.TestCase): - def test(self): - mock_xml = BytesIO( - b""" - - - - - """ - ) - - with patch("urllib.request.urlopen") as mock_urlopen: - mock_urlopen.return_value = mock_xml - lst = ofxhome.list_institutions() - - self.assertEqual( - lst, - OrderedDict([("1234", "American Express"), ("2222", "Bank of America")]), - ) - - -class LookupTestCase(unittest.TestCase): - def test(self): - mock_xml = BytesIO( - b""" - - American Express Card - 3101 - AMEX - https://online.americanexpress.com/myca/ofxdl/desktop/desktopDownload.do?request_type=nl_ofxdownload - 0 - 0 - 2019-04-29 23:08:45 - 2019-04-29 23:08:44 - - - """ - ) - - with patch("urllib.request.urlopen") as mock_urlopen: - mock_urlopen.return_value = mock_xml - lookup = ofxhome.lookup("424") - - self.assertIsInstance(lookup, ofxhome.OFXServer) - self.assertEqual(lookup.id, "424") - self.assertEqual(lookup.name, "American Express Card") - self.assertEqual(lookup.fid, "3101") - self.assertEqual(lookup.org, "AMEX") - self.assertEqual( - lookup.url, - "https://online.americanexpress.com/myca/ofxdl/desktop/desktopDownload.do?request_type=nl_ofxdownload", - ) - self.assertEqual(lookup.ofxfail, False) - self.assertEqual(lookup.sslfail, False) - self.assertEqual(lookup.lastofxvalidation, datetime(2019, 4, 29, 23, 8, 45)) - self.assertEqual(lookup.lastsslvalidation, datetime(2019, 4, 29, 23, 8, 44)) - self.assertEqual( - lookup.profile, - { - "finame": "American Express", - "addr1": "777 American Expressway", - "city": "Fort Lauderdale", - "state": "Fla.", - "postalcode": "33337-0001", - "country": "USA", - "csphone": "1-800-AXP-7500 (1-800-297-7500)", - "url": "https://online.americanexpress.com/myca/ofxdl/desktop/desktopDownload.do?request_type=nl_ofxdownload", - "signonmsgset": True, - "bankmsgset": True, - "creditcardmsgset": True, - }, - ) - - -class FetchFiXmlTestCase(unittest.TestCase): - pass - - -class OfxInvalidTestCase(unittest.TestCase): - pass - - -class SslInvalidTestCase(unittest.TestCase): - pass - - -if __name__ == "__main__": - unittest.main()