From 87886589f32c3b6d705244bd00574e49e68eeeb6 Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Wed, 6 Nov 2024 14:12:01 +0100 Subject: [PATCH 01/12] update deps and fmt --- .gitignore | 3 +- .vscode/launch.json | 18 +++ .vscode/settings.json | 13 +++ Makefile | 13 +++ gen_data.py | 224 ++++++++++++++++++++------------------ iso4217parse/__init__.py | 127 +++++++++++---------- pyproject.toml | 35 ++---- setup.cfg | 3 +- tests/test_by_alpha3.py | 17 ++- tests/test_by_code_num.py | 14 ++- tests/test_by_country.py | 40 +++++-- tests/test_by_symbol.py | 29 +++-- tests/test_parse.py | 84 ++++++++++---- 13 files changed, 378 insertions(+), 242 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 Makefile diff --git a/.gitignore b/.gitignore index 7f41a4e..53d633c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,11 +9,12 @@ __pycache__/ # macOS .DS_Store -.vscode/ +.ruff_cache/ dist/ pyproject.lock poetry.lock cov_html/ +coverage.xml .pytest_cache/ .hypothesis/ .venv/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..dd2851e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Debug Tests", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "purpose": ["debug-test"], + // "console": "integratedTerminal", + "justMyCode": false, + "env": {"PYTHONDEVMODE": "1"} + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9607b27 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "python.testing.pytestArgs": [ + "tests", + "--no-cov", + "-s", + "-vvv" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff" + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..aac582b --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +.PHONY: fmt check test + +fmt: + poetry run ruff format . + poetry run ruff check --fix . + +check: + poetry run ruff format --check . + poetry run ruff check . + # poetry run mypy src + +test: + PYTHONDEVMODE=1 poetry run pytest -vvv -s diff --git a/gen_data.py b/gen_data.py index a68a24f..64fb830 100644 --- a/gen_data.py +++ b/gen_data.py @@ -38,18 +38,18 @@ import requests # symbols see subsequent pages and http://www.xe.com/currency/ -res = requests.get('https://en.wikipedia.org/wiki/ISO_4217') -soup = BeautifulSoup(res.content, 'lxml') +res = requests.get("https://en.wikipedia.org/wiki/ISO_4217") +soup = BeautifulSoup(res.content, "lxml") -tables = soup.findAll('table') +tables = soup.findAll("table") if len(sys.argv) < 2: - print('Use like: python3 {} []'.format(sys.argv[0])) + print("Use like: python3 {} []".format(sys.argv[0])) sys.exit(42) p = Path(sys.argv[1]).absolute() if not p.is_dir(): - print('Use like: python3 {} []'.format(sys.argv[0])) + print("Use like: python3 {} []".format(sys.argv[0])) sys.exit(42) tmp_out = False @@ -58,66 +58,73 @@ # some names do not resolve with iso3166 package; or are special alt_iso3166 = { - 'Bolivia': 'BO', - 'Democratic Republic of the Congo': 'CD', - 'Somaliland': 'SO', - 'Transnistria': 'MD', - 'Venezuela': 'VE', - 'Caribbean Netherlands': 'BQ', - 'British Virgin Islands': 'VG', - 'Federated States of Micronesia': 'FM', - 'U.S. Virgin Islands': 'VI', - 'Republic of the Congo': 'CG', - 'Sint Maarten': 'SX', - 'Cocos Islands': 'CC', - 'the Isle of Man': 'IM', - 'and Guernsey': 'GG', - 'Pitcairn Islands': 'PN', - 'French territories of the Pacific Ocean: French Polynesia': 'PF', - 'Kosovo': 'XK' + "Bolivia": "BO", + "Democratic Republic of the Congo": "CD", + "Somaliland": "SO", + "Transnistria": "MD", + "Venezuela": "VE", + "Caribbean Netherlands": "BQ", + "British Virgin Islands": "VG", + "Federated States of Micronesia": "FM", + "U.S. Virgin Islands": "VI", + "Republic of the Congo": "CG", + "Sint Maarten": "SX", + "Cocos Islands": "CC", + "the Isle of Man": "IM", + "and Guernsey": "GG", + "Pitcairn Islands": "PN", + "French territories of the Pacific Ocean: French Polynesia": "PF", + "Kosovo": "XK", } additional_countries = { - 'EUR': ['Åland Islands', 'French Guiana', 'French Southern Territories', 'Holy See', - 'Saint Martin (French part)'], - 'SEK': ['Åland Islands'], - 'EGP': ['Palestine, State of'], # see https://en.wikipedia.org/wiki/State_of_Palestine - 'ILS': ['Palestine, State of'], - 'JOD': ['Palestine, State of'], - 'FKP': ['South Georgia and the South Sandwich Islands'], + "EUR": [ + "Åland Islands", + "French Guiana", + "French Southern Territories", + "Holy See", + "Saint Martin (French part)", + ], + "SEK": ["Åland Islands"], + "EGP": [ + "Palestine, State of" + ], # see https://en.wikipedia.org/wiki/State_of_Palestine + "ILS": ["Palestine, State of"], + "JOD": ["Palestine, State of"], + "FKP": ["South Georgia and the South Sandwich Islands"], # 'Sahrawi peseta': ['Western Sahara'], - 'MAD': ['Western Sahara'], - 'DZD': ['Western Sahara'], - 'MRO': ['Western Sahara'], + "MAD": ["Western Sahara"], + "DZD": ["Western Sahara"], + "MRO": ["Western Sahara"], } # get active table active = [] -for row in tables[1].findAll('tr'): # noqa - tds = row.findAll('td') +for row in tables[1].findAll("tr"): # noqa + tds = row.findAll("td") if tds: try: - minor = int(re.sub(r'\[[0-9]+\]', r'', tds[2].text.replace('*', ''))) - except: + minor = int(re.sub(r"\[[0-9]+\]", r"", tds[2].text.replace("*", ""))) + except: # noqa: E722 minor = 0 d = dict( code=tds[0].text, code_num=int(tds[1].text), minor=minor, - name=re.sub(r'\[[0-9]+\]', r'', tds[3].text).strip(), - countries=tds[4].text.replace('\xa0', ''), + name=re.sub(r"\[[0-9]+\]", r"", tds[3].text).strip(), + countries=tds[4].text.replace("\xa0", ""), ) - d['countries'] = re.sub(r'\([^)]+\)', r' ', d['countries']) - d['countries'] = re.sub(r'\[[0-9]+\]', r' ', d['countries']).strip() - d['countries'] = [c.strip() for c in d['countries'].split(',') if c] - if d['code'] in additional_countries: - d['countries'] += additional_countries[d['code']] + d["countries"] = re.sub(r"\([^)]+\)", r" ", d["countries"]) + d["countries"] = re.sub(r"\[[0-9]+\]", r" ", d["countries"]).strip() + d["countries"] = [c.strip() for c in d["countries"].split(",") if c] + if d["code"] in additional_countries: + d["countries"] += additional_countries[d["code"]] ccodes = [] - for c in d['countries']: + for c in d["countries"]: if c in alt_iso3166: ccodes += [alt_iso3166[c]] - m = re.match(r'.*\(([A-Z]{2})\).*', c) + m = re.match(r".*\(([A-Z]{2})\).*", c) if m: ccodes += [m.group(1)] else: @@ -125,131 +132,140 @@ if code: ccodes += [code.alpha2] else: - code = iso3166.countries.get(d['code_num'], None) + code = iso3166.countries.get(d["code_num"], None) if code: ccodes += [code.alpha2] - if len(d['countries']) != len(set(ccodes)) and d['code'] not in {'SHP', 'XDR', 'XSU', 'XUA'}: - print(d['countries'], set(ccodes)) - d['country_codes'] = sorted(set(ccodes)) + if len(d["countries"]) != len(set(ccodes)) and d["code"] not in { + "SHP", + "XDR", + "XSU", + "XUA", + }: + print(d["countries"], set(ccodes)) + d["country_codes"] = sorted(set(ccodes)) active += [d] if tmp_out: - with open(f'{p}/active.json', 'w') as f: + with open(f"{p}/active.json", "w") as f: json.dump(active, f, indent=4, sort_keys=True, ensure_ascii=False) # get unofficials table unofficial = [] -for row in tables[2].findAll('tr'): - tds = row.findAll('td') +for row in tables[2].findAll("tr"): + tds = row.findAll("td") if tds: try: - minor = int(re.sub(r'\[[0-9]+\]', r'', tds[2].text.replace('*', ''))) - except: + minor = int(re.sub(r"\[[0-9]+\]", r"", tds[2].text.replace("*", ""))) + except: # noqa: E722 minor = 0 d = dict( code=tds[0].text, minor=minor, - name=re.sub(r'\[[0-9]+\]', r'', tds[3].find('a').text).strip(), - countries=[a.text.strip() for a in tds[4].findAll('a')]) - d['countries'] = [re.sub(r'\([^)]+\)', r'', c) for c in d['countries']] - d['countries'] = [re.sub(r'\[[0-9]+\]', r'', c).strip() for c in d['countries']] - d['countries'] = [c for c in d['countries'] if c] + name=re.sub(r"\[[0-9]+\]", r"", tds[3].find("a").text).strip(), + countries=[a.text.strip() for a in tds[4].findAll("a")], + ) + d["countries"] = [re.sub(r"\([^)]+\)", r"", c) for c in d["countries"]] + d["countries"] = [re.sub(r"\[[0-9]+\]", r"", c).strip() for c in d["countries"]] + d["countries"] = [c for c in d["countries"] if c] ccodes = [] - for c in d['countries']: + for c in d["countries"]: if c in alt_iso3166: ccodes += [alt_iso3166[c]] - m = re.match(r'.*\(([A-Z]{2})\).*', c) + m = re.match(r".*\(([A-Z]{2})\).*", c) if m: ccodes += [m.group(1)] else: code = iso3166.countries.get(c, None) if code: ccodes += [code.alpha2] - d['code'] = re.sub(r'\[[0-9]+\]', r'', d['code']) - d['country_codes'] = sorted(set(ccodes)) + d["code"] = re.sub(r"\[[0-9]+\]", r"", d["code"]) + d["country_codes"] = sorted(set(ccodes)) unofficial += [d] if tmp_out: - with open(f'{p}/unofficial.json', 'w') as f: + with open(f"{p}/unofficial.json", "w") as f: json.dump(unofficial, f, indent=4, sort_keys=True, ensure_ascii=False) # ignore historical for now historical = [] -for row in tables[5].findAll('tr'): # noqa - tds = row.findAll('td') +for row in tables[5].findAll("tr"): # noqa + tds = row.findAll("td") if tds: code = tds[1].text if code.isnumeric(): code = int(code) try: - minor = int(re.sub(r'\[[0-9]+\]', r'', tds[2].text.replace('*', ''))) - except: + minor = int(re.sub(r"\[[0-9]+\]", r"", tds[2].text.replace("*", ""))) + except: # noqa: E722 minor = 0 - from_ = re.sub(r'\[[0-9]+\]', r'', tds[4].text).strip() + from_ = re.sub(r"\[[0-9]+\]", r"", tds[4].text).strip() try: from_ = parse(from_).year - except: - if from_ == '?': + except: # noqa: E722 + if from_ == "?": from_ = None - until = re.sub(r'\[[0-9]+\]', r'', tds[5].text).strip() + until = re.sub(r"\[[0-9]+\]", r"", tds[5].text).strip() try: until = parse(until).year - except: - if until == '?': + except: # noqa: E722 + if until == "?": until = None replace = tds[6].text.strip().split() if len(replace) == 1: - direct_replace = replace[0].split('/') - valid_replace = replace[0].split('/') + direct_replace = replace[0].split("/") + valid_replace = replace[0].split("/") elif len(replace) == 2: - direct_replace = replace[0].split('/') - valid_replace = replace[1].replace('(', '').replace(')', '').split('/') - - historical += [dict( - code=tds[0].text, - code_num=None if code == '...' else code, - minor=minor, - name=re.sub(r'\[[0-9]+\]', r'', tds[3].text).strip(), - from_=from_, - until=until, - direct_replaced=direct_replace, - valid_replaced=valid_replace)] + direct_replace = replace[0].split("/") + valid_replace = replace[1].replace("(", "").replace(")", "").split("/") + + historical += [ + dict( + code=tds[0].text, + code_num=None if code == "..." else code, + minor=minor, + name=re.sub(r"\[[0-9]+\]", r"", tds[3].text).strip(), + from_=from_, + until=until, + direct_replaced=direct_replace, + valid_replaced=valid_replace, + ) + ] if tmp_out: - with open(f'{p}/historical.json', 'w') as f: + with open(f"{p}/historical.json", "w") as f: json.dump(historical, f, indent=4, sort_keys=True, ensure_ascii=False) -with open('{}/symbols.json'.format(p), 'r') as f: +with open("{}/symbols.json".format(p), "r") as f: symbols = json.load(f) data = dict() for d in active: - data[d['code']] = dict( - name=d['name'], - alpha3=d['code'], - code_num=d['code_num'], - countries=d['country_codes'], - minor=d['minor'], - symbols=symbols.get(d['code'], []) + data[d["code"]] = dict( + name=d["name"], + alpha3=d["code"], + code_num=d["code_num"], + countries=d["country_codes"], + minor=d["minor"], + symbols=symbols.get(d["code"], []), ) for d in unofficial: - data[d['code']] = dict( - name=d['name'], - alpha3=d['code'], + data[d["code"]] = dict( + name=d["name"], + alpha3=d["code"], code_num=None, # d['code_num'], - countries=d['country_codes'], - minor=d['minor'], - symbols=symbols.get(d['code'], []) + countries=d["country_codes"], + minor=d["minor"], + symbols=symbols.get(d["code"], []), ) -with open('{}/data.json'.format(p), 'w') as f: +with open("{}/data.json".format(p), "w") as f: json.dump(data, f, sort_keys=True, ensure_ascii=False, indent=4) diff --git a/iso4217parse/__init__.py b/iso4217parse/__init__.py index 78130b4..3b79414 100644 --- a/iso4217parse/__init__.py +++ b/iso4217parse/__init__.py @@ -24,11 +24,11 @@ # THE SOFTWARE. from collections import defaultdict, namedtuple +import importlib.resources import json import re import sys -from pkg_resources import resource_filename _PY3 = sys.version_info[0] == 3 @@ -39,24 +39,27 @@ __all__ = [ - 'Currency', - 'by_alpha3', - 'by_code_num', - 'by_symbol', - 'by_symbol_match', - 'by_country', - 'parse', + "Currency", + "by_alpha3", + "by_code_num", + "by_symbol", + "by_symbol_match", + "by_country", + "parse", ] -Currency = namedtuple('Currency', [ - 'alpha3', # unicode: the ISO4217 alpha3 code - 'code_num', # int: the ISO4217 numeric code - 'name', # unicode: the currency name - 'symbols', # List[unicode]: list of possible symbols; - # first is opinionated choice for representation - 'minor', # int: number of decimal digits to round - 'countries', # List[unicode]: list of countries that use this currency. -]) +Currency = namedtuple( + "Currency", + [ + "alpha3", # unicode: the ISO4217 alpha3 code + "code_num", # int: the ISO4217 numeric code + "name", # unicode: the currency name + "symbols", # List[unicode]: list of possible symbols; + # first is opinionated choice for representation + "minor", # int: number of decimal digits to round + "countries", # List[unicode]: list of countries that use this currency. + ], +) _DATA = None @@ -75,35 +78,42 @@ def _data(): global _DATA if _DATA is None: _DATA = {} - with open(resource_filename(__name__, 'data.json'), 'r', encoding='utf-8') as f: - _DATA['alpha3'] = {k: Currency(**v) for k, v in json.load(f).items()} - - _DATA['code_num'] = {d.code_num: d for d in _DATA['alpha3'].values() if d.code_num is not None} - _DATA['symbol'] = defaultdict(list) - for d in _DATA['alpha3'].values(): + with importlib.resources.open_text("iso4217parse", "data.json") as f: + _DATA["alpha3"] = {k: Currency(**v) for k, v in json.load(f).items()} + + _DATA["code_num"] = { + d.code_num: d for d in _DATA["alpha3"].values() if d.code_num is not None + } + _DATA["symbol"] = defaultdict(list) + for d in _DATA["alpha3"].values(): for s in d.symbols: - _DATA['symbol'][s] += [d] + _DATA["symbol"][s] += [d] - for s, d in _DATA['symbol'].items(): - _DATA['symbol'][s] = sorted(d, key=lambda d: 10000 if d.code_num is None else d.code_num) + for s, d in _DATA["symbol"].items(): + _DATA["symbol"][s] = sorted( + d, key=lambda d: 10000 if d.code_num is None else d.code_num + ) - _DATA['name'] = {} - for d in _DATA['alpha3'].values(): - if d.name in _DATA['name']: + _DATA["name"] = {} + for d in _DATA["alpha3"].values(): + if d.name in _DATA["name"]: assert 'Duplicate name "{}"!'.format(d.name) - _DATA['name'][d.name] = d + _DATA["name"][d.name] = d - _DATA['country'] = defaultdict(list) - for d in _DATA['alpha3'].values(): + _DATA["country"] = defaultdict(list) + for d in _DATA["alpha3"].values(): for cc in d.countries: - _DATA['country'][cc] += [d] - - for s, d in _DATA['country'].items(): - _DATA['country'][s] = sorted(d, key=lambda d: ( - int(d.symbols == []), # at least one symbol - 10000 if d.code_num is None else d.code_num, # official first - len(d.countries), # the fewer countries the more specific - )) + _DATA["country"][cc] += [d] + + for s, d in _DATA["country"].items(): + _DATA["country"][s] = sorted( + d, + key=lambda d: ( + int(d.symbols == []), # at least one symbol + 10000 if d.code_num is None else d.code_num, # official first + len(d.countries), # the fewer countries the more specific + ), + ) return _DATA @@ -119,13 +129,10 @@ def _symbols(): """ global _SYMBOLS if _SYMBOLS is None: - tmp = [(s, 'symbol') for s in _data()['symbol'].keys()] - tmp += [(s, 'alpha3') for s in _data()['alpha3'].keys()] - tmp += [(s.name, 'name') for s in _data()['alpha3'].values()] - _SYMBOLS = sorted( - tmp, - key=lambda s: (len(s[0]), ord(s[0][0])), - reverse=True) + tmp = [(s, "symbol") for s in _data()["symbol"].keys()] + tmp += [(s, "alpha3") for s in _data()["alpha3"].keys()] + tmp += [(s.name, "name") for s in _data()["alpha3"].values()] + _SYMBOLS = sorted(tmp, key=lambda s: (len(s[0]), ord(s[0][0])), reverse=True) return _SYMBOLS @@ -139,7 +146,7 @@ def by_alpha3(code): Returns: Currency: Currency object for `code`, if available. """ - return _data()['alpha3'].get(code) + return _data()["alpha3"].get(code) def by_code_num(code_num): @@ -151,7 +158,7 @@ def by_code_num(code_num): Returns: Currency: return Currency object for `code_num`, if available. """ - return _data()['code_num'].get(code_num) + return _data()["code_num"].get(code_num) def by_symbol(symbol, country_code=None): @@ -168,7 +175,7 @@ def by_symbol(symbol, country_code=None): Returns: List[Currency]: Currency objects for `symbol`; filter by country_code. """ - res = _data()['symbol'].get(symbol) + res = _data()["symbol"].get(symbol) if res: tmp_res = [] for d in res: @@ -202,17 +209,15 @@ def by_symbol_match(value, country_code=None): for symbol, group in _symbols(): symbol_pattern = re.escape(symbol) if re.search(rf"(^|\b|\d|\s){symbol_pattern}([^A-Z]|$)", value, re.I): - if group == 'symbol': + if group == "symbol": res = by_symbol(symbol, country_code) - if group == 'alpha3': + if group == "alpha3": res = [by_alpha3(symbol)] - if group == 'name': - res = [_data()['name'][symbol]] + if group == "name": + res = [_data()["name"][symbol]] if res and country_code is not None: res = [ - currency - for currency in res - if country_code in currency.countries + currency for currency in res if country_code in currency.countries ] if res: return res @@ -228,7 +233,7 @@ def by_country(country_code): List[Currency]: Currency objects used in country. """ - return _data()['country'].get(country_code) + return _data()["country"].get(country_code) def parse(v, country_code=None): @@ -252,10 +257,12 @@ def parse(v, country_code=None): return [] if not res else [res] if not isinstance(v, (str, unicode)): - raise ValueError('`v` of incorrect type {}. Only accepts str, bytes, unicode and int.') + raise ValueError( + "`v` of incorrect type {}. Only accepts str, bytes, unicode and int." + ) # check alpha3 - if re.match('^[A-Z]{3}$', v): + if re.match("^[A-Z]{3}$", v): res = by_alpha3(v) if res: return [res] diff --git a/pyproject.toml b/pyproject.toml index e2b3623..c876362 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "iso4217parse" -version = "0.5.1" +version = "0.6.0" description = "Parse currencies (symbols and codes) from and to ISO4217." authors = ["Tammo Ippen "] license = "MIT" @@ -20,34 +20,21 @@ classifiers=[ # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy' ] [tool.poetry.dependencies] -python = "~2.7 | ^3.5" +python = "^3.9" [tool.poetry.dev-dependencies] -coveralls = "^1.5.1" -flake8 = "^3.5.0" -flake8-bugbear = { version = "^18.8.0", python = "^3.5" } -flake8-commas = "^2.0.0" -flake8-comprehensions = "^1.4.1" -flake8-import-order = "^0.18" -flake8-pep3101 = "^1.2.1" -flake8-polyfill = "^1.0.2" -flake8-quotes = "^1.0.0" -funcsigs = { version = "^1.0", python = "~2.7" } -iso3166 = "^0.9.0" -pep8-naming = "^0.7" -pytest = "^3.7.3" -pytest-cov = "^2.5.1" -pytest-flake8 = "^1.0.2" -pytest-pythonpath = "^0.7.3" -six = "^1.11.0" +coveralls = "*" +iso3166 = "^2" +pytest = "^8" +pytest-cov = "^6" +ruff = "*" diff --git a/setup.cfg b/setup.cfg index 61efb0c..daefa45 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,9 +9,8 @@ exclude = gen_data.py [tool:pytest] -python_paths = . ignore = gen_data.py -addopts = --cov=iso4217parse --cov-report term-missing --cov-report html:cov_html -v +addopts = --cov=iso4217parse --cov-branch --cov-report term-missing --cov-report html:cov_html --cov-report=xml:coverage.xml -v [bdist_wheel] universal = 1 diff --git a/tests/test_by_alpha3.py b/tests/test_by_alpha3.py index 663133a..046c02f 100644 --- a/tests/test_by_alpha3.py +++ b/tests/test_by_alpha3.py @@ -7,16 +7,21 @@ def test_invalid(): assert iso4217parse.by_alpha3(None) is None assert iso4217parse.by_alpha3(1234) is None - assert iso4217parse.by_alpha3('Blaa') is None + assert iso4217parse.by_alpha3("Blaa") is None def test_all_currencies(): - for code in iso4217parse._data()['alpha3'].keys(): + for code in iso4217parse._data()["alpha3"].keys(): assert isinstance(iso4217parse.by_alpha3(code), iso4217parse.Currency) def test_examples(): - exp = iso4217parse.Currency(alpha3='CHF', code_num=756, name='Swiss franc', - symbols=['SFr.', 'fr', 'Fr.', 'F', 'franc', 'francs', 'Franc', 'Francs'], - minor=2, countries=['CH', 'LI']) - assert exp == iso4217parse.by_alpha3('CHF') + exp = iso4217parse.Currency( + alpha3="CHF", + code_num=756, + name="Swiss franc", + symbols=["SFr.", "fr", "Fr.", "F", "franc", "francs", "Franc", "Francs"], + minor=2, + countries=["CH", "LI"], + ) + assert exp == iso4217parse.by_alpha3("CHF") diff --git a/tests/test_by_code_num.py b/tests/test_by_code_num.py index 9c5e644..267a44b 100644 --- a/tests/test_by_code_num.py +++ b/tests/test_by_code_num.py @@ -7,15 +7,21 @@ def test_invalid(): assert iso4217parse.by_code_num(None) is None assert iso4217parse.by_code_num(1234) is None - assert iso4217parse.by_code_num('Blaa') is None + assert iso4217parse.by_code_num("Blaa") is None def test_all_currencies(): - for code in iso4217parse._data()['code_num'].keys(): + for code in iso4217parse._data()["code_num"].keys(): assert isinstance(iso4217parse.by_code_num(code), iso4217parse.Currency) def test_examples(): - exp = iso4217parse.Currency(alpha3='AMD', code_num=51, name='Armenian dram', - symbols=['֏', 'դր', 'dram'], minor=2, countries=['AM']) + exp = iso4217parse.Currency( + alpha3="AMD", + code_num=51, + name="Armenian dram", + symbols=["֏", "դր", "dram"], + minor=2, + countries=["AM"], + ) assert exp == iso4217parse.by_code_num(51) diff --git a/tests/test_by_country.py b/tests/test_by_country.py index 680f1d4..37aafec 100644 --- a/tests/test_by_country.py +++ b/tests/test_by_country.py @@ -9,12 +9,12 @@ def test_invalid(): assert iso4217parse.by_country(None) is None assert iso4217parse.by_country(1234) is None - assert iso4217parse.by_country('Blaa') is None + assert iso4217parse.by_country("Blaa") is None def test_all_countries(): for country in iso3166._records: - if 'AQ' == country.alpha2: # ignore Antarctica + if "AQ" == country.alpha2: # ignore Antarctica continue cs = iso4217parse.by_country(country.alpha2) if cs is None: @@ -25,11 +25,33 @@ def test_all_countries(): def test_examples(): exp = [ - iso4217parse.Currency(alpha3='HKD', code_num=344, name='Hong Kong dollar', - symbols=['HK$', 'HK$', '$', '$', 'dollar', 'dollars', 'Dollar', 'Dollars', 'HK﹩', '﹩', '元'], - minor=2, countries=['HK']), - iso4217parse.Currency(alpha3='CNH', code_num=None, name='Chinese yuan', - symbols=['CN¥', '¥', 'CN¥', '¥', 'RMB', '元'], - minor=2, countries=['HK']), + iso4217parse.Currency( + alpha3="HKD", + code_num=344, + name="Hong Kong dollar", + symbols=[ + "HK$", + "HK$", + "$", + "$", + "dollar", + "dollars", + "Dollar", + "Dollars", + "HK﹩", + "﹩", + "元", + ], + minor=2, + countries=["HK"], + ), + iso4217parse.Currency( + alpha3="CNH", + code_num=None, + name="Chinese yuan", + symbols=["CN¥", "¥", "CN¥", "¥", "RMB", "元"], + minor=2, + countries=["HK"], + ), ] - assert exp == iso4217parse.by_country('HK') + assert exp == iso4217parse.by_country("HK") diff --git a/tests/test_by_symbol.py b/tests/test_by_symbol.py index 30b3caa..172d2f4 100644 --- a/tests/test_by_symbol.py +++ b/tests/test_by_symbol.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import ( - absolute_import, division, print_function, + absolute_import, + division, + print_function, unicode_literals, ) @@ -10,11 +12,11 @@ @pytest.mark.parametrize( - 'symbol, country_code', + "symbol, country_code", ( - ('$', 'DOESNT_EXIST'), - ('£', 'DOESNT_EXIST'), - ('€', 'DOESNT_EXIST'), + ("$", "DOESNT_EXIST"), + ("£", "DOESNT_EXIST"), + ("€", "DOESNT_EXIST"), ), ) def test_symbol_match_ignored_without_country_match(symbol, country_code): @@ -22,17 +24,20 @@ def test_symbol_match_ignored_without_country_match(symbol, country_code): @pytest.mark.parametrize( - 'invalid1, invalid2, valid, country_code, expected', + "invalid1, invalid2, valid, country_code, expected", ( - ('$', 'Pounds Sterling', 'EUR', 'FR', 'EUR'), - ('Euro', 'USD', '£', 'GB', 'GBP'), - ('GBP', '€', 'United States dollar', 'US', 'USD'), + ("$", "Pounds Sterling", "EUR", "FR", "EUR"), + ("Euro", "USD", "£", "GB", "GBP"), + ("GBP", "€", "United States dollar", "US", "USD"), ), ) def test_by_symbol_match_filters_country_code( - invalid1, invalid2, valid, country_code, expected): - example_string = 'You cannot pay in {} or {}. The price is {}3.25'.format( - invalid1, invalid2, valid, + invalid1, invalid2, valid, country_code, expected +): + example_string = "You cannot pay in {} or {}. The price is {}3.25".format( + invalid1, + invalid2, + valid, ) res = iso4217parse.by_symbol_match(example_string, country_code) assert len(res) == 1 diff --git a/tests/test_parse.py b/tests/test_parse.py index 5920848..56a3e38 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -13,7 +13,7 @@ def test_invalid(): def test_examples_code(): - for code, exp in iso4217._data()['alpha3'].items(): + for code, exp in iso4217._data()["alpha3"].items(): assert [exp] == iso4217.parse(code) @@ -28,36 +28,80 @@ def test_examples_numbers(): def test_examples_EUR(): exp = iso4217.Currency( - alpha3='EUR', + alpha3="EUR", code_num=978, - name='Euro', - symbols=['€', 'euro', 'euros'], + name="Euro", + symbols=["€", "euro", "euros"], minor=2, - countries=['AD', 'AT', 'AX', 'BE', 'BL', 'CY', 'DE', 'EE', 'ES', 'FI', - 'FR', 'GF', 'GP', 'GR', 'IE', 'IT', 'LT', 'LU', 'LV', 'MC', - 'ME', 'MF', 'MQ', 'MT', 'NL', 'PM', 'PT', 'RE', 'SI', 'SK', - 'SM', 'TF', 'VA', 'XK', 'YT'], + countries=[ + "AD", + "AT", + "AX", + "BE", + "BL", + "CY", + "DE", + "EE", + "ES", + "FI", + "FR", + "GF", + "GP", + "GR", + "IE", + "IT", + "LT", + "LU", + "LV", + "MC", + "ME", + "MF", + "MQ", + "MT", + "NL", + "PM", + "PT", + "RE", + "SI", + "SK", + "SM", + "TF", + "VA", + "XK", + "YT", + ], ) - assert [exp] == iso4217.parse('Price is 5 €') - assert [exp] == iso4217.parse('Price is 5 EUR') - assert [exp] == iso4217.parse('Price is 5 eur') - assert [exp] == iso4217.parse('Price is 5 euro') - assert [exp] == iso4217.parse('Price is 5 Euro') + assert [exp] == iso4217.parse("Price is 5 €") + assert [exp] == iso4217.parse("Price is 5 EUR") + assert [exp] == iso4217.parse("Price is 5 eur") + assert [exp] == iso4217.parse("Price is 5 euro") + assert [exp] == iso4217.parse("Price is 5 Euro") exp = iso4217.Currency( - alpha3='CAD', + alpha3="CAD", code_num=124, - name='Canadian dollar', - symbols=['CA$', 'CA$', '$', '$', 'dollar', 'dollars', 'Dollar', 'Dollars', 'CA﹩', '﹩'], + name="Canadian dollar", + symbols=[ + "CA$", + "CA$", + "$", + "$", + "dollar", + "dollars", + "Dollar", + "Dollars", + "CA﹩", + "﹩", + ], minor=2, - countries=['CA'], + countries=["CA"], ) - assert [exp] == iso4217.parse('CA﹩15.76') + assert [exp] == iso4217.parse("CA﹩15.76") def test_examples_CZK(): - expect = iso4217.by_alpha3('CZK') + expect = iso4217.by_alpha3("CZK") - assert [expect] == iso4217.parse('1499 CZK') + assert [expect] == iso4217.parse("1499 CZK") From ca6705be4099c5fa8a5bd328b414dee98a15c165 Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Wed, 6 Nov 2024 14:18:47 +0100 Subject: [PATCH 02/12] update CI --- .circleci/config.yml | 178 --------------------------------------- .github/workflows/CI.yml | 79 +++++++++++++++++ README.md | 37 ++++---- 3 files changed, 101 insertions(+), 193 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/CI.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 3e8d5df..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,178 +0,0 @@ -version: 2.1 - -commands: - tester: - description: "Test iso4217parse given a certain python version." - steps: - - checkout - - - run: - name: Set PATH. - command: | - echo 'export PATH=$HOME/.local/bin:$PATH' >> $BASH_ENV - source $BASH_ENV - - - restore_cache: - key: v1-iso4217parse-{{ .Environment.CIRCLE_STAGE }}-{{ checksum "pyproject.toml" }}-{{ checksum "setup.cfg" }} - - - run: - name: Pre-install - command: | - pip install --user poetry virtualenv - virtualenv venv - - - run: - name: Install - command: | - . venv/bin/activate - poetry install - - - save_cache: - key: v1-iso4217parse-{{ .Environment.CIRCLE_STAGE }}-{{ checksum "pyproject.toml" }}-{{ checksum "setup.cfg" }} - paths: - - ~/app/venv - - ~/app/poetry.lock - - - store_artifacts: - path: ~/app/poetry.lock - destination: poetry.lock - - - run: - name: Style - command: | - . venv/bin/activate - poetry run flake8 - - - run: - name: Test - command: | - . venv/bin/activate - poetry run pytest - - - run: - name: Build sdist - command: poetry build -vvv -f sdist - - - store_artifacts: - path: ~/app/dist - destination: dist - - - store_artifacts: - path: ~/app/cov_html - destination: cov_html - - - run: - name: Coverage - command: | - . venv/bin/activate - poetry run coveralls - -executors: - python: - working_directory: ~/app - parameters: - image: - type: string - default: latest - docker: - - image: << parameters.image >> - -jobs: - test_2_7: - executor: - name: python - image: "circleci/python:2.7.15" - steps: - - tester - test_2_7_pypy: - executor: - name: python - image: "pypy:2-6.0.0" - steps: - - tester - test_3_5: - executor: - name: python - image: "circleci/python:3.5.6" - steps: - - tester - test_3_5_pypy: - executor: - name: python - image: "pypy:3-6.0.0" - steps: - - tester - test_3_6: - executor: - name: python - image: "circleci/python:3.6.7" - steps: - - tester - test_3_7: - executor: - name: python - image: "circleci/python:3.7.1" - steps: - - tester - - deploy_job: - docker: - - image: circleci/python:3.6.7 - description: "Deploy iso4217parse to pypi." - steps: - - checkout - - - run: - name: Set PATH. - command: | - echo 'export PATH=$HOME/.local/bin:$PATH' >> $BASH_ENV - source $BASH_ENV - - - run: - name: Pre-install - command: | - pip install --user poetry - poetry build -vvv -f sdist - poetry publish -vvv -n -u tammoippen -p $PYPI_PASS - -workflows: - version: 2.1 - test_and_deploy: - jobs: - - test_2_7: - filters: - tags: - only: /.*/ - - test_2_7_pypy: - filters: - tags: - only: /.*/ - - test_3_5: - filters: - tags: - only: /.*/ - - test_3_5_pypy: - filters: - tags: - only: /.*/ - - test_3_6: - filters: - tags: - only: /.*/ - - test_3_7: - filters: - tags: - only: /.*/ - - deploy_job: - requires: - - test_2_7 - - test_2_7_pypy - - test_3_5 - - test_3_5_pypy - - test_3_6 - - test_3_7 - filters: - branches: - ignore: /.*/ - tags: - only: /v[0-9]+(\.[0-9]+)*/ diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..b6856cb --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,79 @@ +name: CI + +on: + push: + branches: + - "master" + pull_request: + branches: + - "master" + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['pypy-3.9', 3.9, '3.10', 'pypy-3.10', '3.11', '3.12', '3.13'] + include: + - os: ubuntu-latest + path: ~/.cache/pip + - os: macos-latest + path: ~/Library/Caches/pip + - os: windows-latest + path: ~\AppData\Local\pip\Cache + exclude: + - os: macos-latest + python-version: 'pypy-3.9' + - os: macos-latest + python-version: 'pypy-3.10' + - os: windows-latest + python-version: 'pypy-3.9' + - os: windows-latest + python-version: 'pypy-3.10' + defaults: + run: + shell: bash + + runs-on: ${{ matrix.os }} + env: + PYTHONIOENCODING: UTF-8 + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set Up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache Install + id: restore-cache + uses: actions/cache@v4 + with: + path: | + ${{ matrix.path }} + poetry.lock + key: ${{ matrix.os }}-${{ matrix.python-version }}-pip-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('tests/requirements.txt') }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: '1.8.3' # ${{ startsWith(matrix.python-version, 'pypy') && '1.2.2' || '1.8.3' }} + virtualenvs-create: false + + - name: Install + run: poetry install + + - name: Style + if: ${{ ! startsWith(matrix.python-version, 'pypy-') }} + run: make check + + - name: Tests + run: make test + + - uses: codecov/codecov-action@v4 + with: + flags: unittests + name: coverage-${{ matrix.os }}-${{ matrix.python-version }} + verbose: true diff --git a/README.md b/README.md index f3f8a43..590290b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![CircleCI](https://circleci.com/gh/tammoippen/iso4217parse.svg?style=svg)](https://circleci.com/gh/tammoippen/iso4217parse) [![Coverage Status](https://coveralls.io/repos/github/tammoippen/iso4217parse/badge.svg?branch=master)](https://coveralls.io/github/tammoippen/iso4217parse?branch=master) -[![Tested CPython Versions](https://img.shields.io/badge/cpython-2.7%2C%203.5%2C%203.6%2C%203.7-brightgreen.svg)](https://img.shields.io/badge/cpython-2.7%2C%203.5%2C%203.6%2C%203.7-brightgreen.svg) -[![Tested PyPy Versions](https://img.shields.io/badge/pypy-2.7--6.0.0%2C%203.5--6.0.0-brightgreen.svg)](https://img.shields.io/badge/pypy-2.7--6.0.0%2C%203.5--6.0.0-brightgreen.svg) +[![Tested CPython Versions](https://img.shields.io/badge/cpython-3.9%2C%203.10%2C%203.11%2C%203.12%2C%203.13-brightgreen.svg)](https://img.shields.io/badge/cpython-3.9%2C%203.10%2C%203.11%2C%203.12%2C%203.13-brightgreen.svg) +[![Tested PyPy Versions](https://img.shields.io/badge/pypy-3.9%2C%203.10-brightgreen.svg)](https://img.shields.io/badge/pypy-3.9%2C%203.10%2C%203.10-brightgreen.svg) [![PyPi version](https://img.shields.io/pypi/v/iso4217parse.svg)](https://pypi.python.org/pypi/iso4217parse) [![PyPi license](https://img.shields.io/pypi/l/iso4217parse.svg)](https://pypi.python.org/pypi/iso4217parse) @@ -11,21 +11,23 @@ Parse currencies (symbols and codes) from and to [ISO4217](https://en.wikipedia. Similar to [iso4217](https://github.com/spoqa/iso4217) package, but - * data is aquired by scraping wikipedia (see [below](#data-aquisition)) - this is repeatable and you stay on the most current data - * currency symbols are currated by hand - this allows some fuzzy currency matching - * no download and parsing during install - * no external dependancies (`enum34`) +- data is aquired by scraping wikipedia (see [below](#data-aquisition)) - this is repeatable and you stay on the most current data +- currency symbols are currated by hand - this allows some fuzzy currency matching +- no download and parsing during install +- no external dependancies (`enum34`) -When you want to *reuse* the [*data.json*](https://github.com/tammoippen/iso4217parse/blob/master/iso4217parse/data.json) file for your projects, please leave a attribution note. I licence the file under (CC BY 4.0). +When you want to _reuse_ the [_data.json_](https://github.com/tammoippen/iso4217parse/blob/master/iso4217parse/data.json) file for your projects, please leave a attribution note. I licence the file under (CC BY 4.0). Install: -``` + +```sh pip install iso4217parse ``` ## Documentation Each currency is modeled as a `collections.namedtuple`: + ```python Currency = namedtuple('Currency', [ 'alpha3', # unicode: the ISO4217 alpha3 code @@ -38,7 +40,8 @@ Currency = namedtuple('Currency', [ ]) ``` -**parse:** Try to parse the input in a best effort approach by using `by_alpha3()`, `by_code_num()`, ... functions: +**parse:** Try to parse the input in a best effort approach by using `by_alpha3()`, `by_code_num()`, ... functions: + ```python In [1]: import iso4217parse @@ -86,6 +89,7 @@ Returns: ``` **by_alpha3:** Get the currency by its iso4217 alpha3 code: + ```python In [1]: import iso4217parse @@ -107,6 +111,7 @@ Returns: ``` **by_code_num:** Get the currency by its iso4217 numeric code: + ```python In [1]: import iso4217parse @@ -127,6 +132,7 @@ Returns: ``` **by_country:** Get currencies used in a country: + ```python In [1]: import iso4217parse @@ -154,6 +160,7 @@ Returns: ``` **by_symbol:** Get currencies that use the given symbol: + ```python In [1]: import iso4217parse @@ -202,6 +209,7 @@ Returns: ``` **by_symbol_match:** Look for currency symbol occurence in input string: + ```python In [1]: import iso4217parse @@ -254,19 +262,18 @@ Returns: List[Currency]: Currency objects found in `value`; filter by country_code. ``` - ## Data aquisition Basic ISO4217 currency information is gathered from wikipedia: https://en.wikipedia.org/wiki/ISO_4217 . The tables are parsed with `gen_data.py` and stored in `iso4217parse/data.json`. This gives information for `alpha3`, `code_num`, `name`, `minor` and `countries`. The currency symbol information is hand gathered from: -* individuel wikipedia pages, i.e. [EUR](https://en.wikipedia.org/wiki/Euro) has a `Denominations` -> `Symbol` section. -* http://www.iotafinance.com/en/ISO-4217-Currency-Codes.html -* http://www.xe.com/currency/ , i.e. [GBP](http://www.xe.com/currency/gbp-british-pound) has a `Currency Facts` -> `Symbol` section +- individuel wikipedia pages, i.e. [EUR](https://en.wikipedia.org/wiki/Euro) has a `Denominations` -> `Symbol` section. +- http://www.iotafinance.com/en/ISO-4217-Currency-Codes.html +- http://www.xe.com/currency/ , i.e. [GBP](http://www.xe.com/currency/gbp-british-pound) has a `Currency Facts` -> `Symbol` section and stored in `iso4217parse/symbols.json`. Each currency can have multiple currency symbols - the first symbol in the list is the (opinionated) choice for the currency. **Contribution Note**: Possible ways to contribute here: -* hand check symbols for currency code. -* automatic generation of the `iso4217parse/symbols.json` file. +- hand check symbols for currency code. +- automatic generation of the `iso4217parse/symbols.json` file. From fb09831939afa721b16af94ce99deaa115a67a5d Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Wed, 6 Nov 2024 14:26:10 +0100 Subject: [PATCH 03/12] fix 2.7 import --- iso4217parse/__init__.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/iso4217parse/__init__.py b/iso4217parse/__init__.py index 3b79414..0fc66f1 100644 --- a/iso4217parse/__init__.py +++ b/iso4217parse/__init__.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division, print_function, unicode_literals - # The MIT License # Copyright (c) 2017 - 2018 Tammo Ippen, tammo.ippen@posteo.de @@ -27,15 +24,6 @@ import importlib.resources import json import re -import sys - - -_PY3 = sys.version_info[0] == 3 - -if _PY3: - unicode = str -else: - from io import open __all__ = [ @@ -256,7 +244,7 @@ def parse(v, country_code=None): res = by_code_num(v) return [] if not res else [res] - if not isinstance(v, (str, unicode)): + if not isinstance(v, str): raise ValueError( "`v` of incorrect type {}. Only accepts str, bytes, unicode and int." ) From 4f4f29428dbe294a9c753aeedcbffead625555fb Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Wed, 6 Nov 2024 14:26:17 +0100 Subject: [PATCH 04/12] copyrught --- gen_data.py | 2 +- iso4217parse/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gen_data.py b/gen_data.py index 64fb830..87b8ba0 100644 --- a/gen_data.py +++ b/gen_data.py @@ -1,6 +1,6 @@ # The MIT License -# Copyright (c) 2017 - 2018 Tammo Ippen, tammo.ippen@posteo.de +# Copyright (c) 2017 - 2024 Tammo Ippen, tammo.ippen@posteo.de # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/iso4217parse/__init__.py b/iso4217parse/__init__.py index 0fc66f1..e3d3097 100644 --- a/iso4217parse/__init__.py +++ b/iso4217parse/__init__.py @@ -1,6 +1,6 @@ # The MIT License -# Copyright (c) 2017 - 2018 Tammo Ippen, tammo.ippen@posteo.de +# Copyright (c) 2017 - 2024 Tammo Ippen, tammo.ippen@posteo.de # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal From 7a6e4c6c9dcc553728e070caf03f6d22ab996a68 Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Wed, 6 Nov 2024 14:29:53 +0100 Subject: [PATCH 05/12] only style check on ubuntu --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b6856cb..169824d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -66,7 +66,7 @@ jobs: run: poetry install - name: Style - if: ${{ ! startsWith(matrix.python-version, 'pypy-') }} + if: ${{ ! startsWith(matrix.python-version, 'pypy-') && startswith(matrix.os, 'ubuntu') }} run: make check - name: Tests From 835f13c3fcb23f8a4a8c2722bb0fbf8f0bce8c15 Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Wed, 6 Nov 2024 14:30:05 +0100 Subject: [PATCH 06/12] typo --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 169824d..4e6cb71 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -66,7 +66,7 @@ jobs: run: poetry install - name: Style - if: ${{ ! startsWith(matrix.python-version, 'pypy-') && startswith(matrix.os, 'ubuntu') }} + if: ${{ ! startsWith(matrix.python-version, 'pypy-') && startsWith(matrix.os, 'ubuntu') }} run: make check - name: Tests From f0156e447c95b3922c1d852b9343f959c59d729a Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Wed, 6 Nov 2024 14:38:54 +0100 Subject: [PATCH 07/12] fix codecov --- .github/workflows/CI.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4e6cb71..32429a2 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v4 - name: Set Up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -74,6 +74,7 @@ jobs: - uses: codecov/codecov-action@v4 with: - flags: unittests + file: coverage.xml name: coverage-${{ matrix.os }}-${{ matrix.python-version }} + token: ${{ secrets.CODECOV_TOKEN }} verbose: true From 92c63fbcfc813bc6a073d89e4bf4541459a3e94a Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Wed, 6 Nov 2024 14:49:44 +0100 Subject: [PATCH 08/12] update readme --- README.md | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 590290b..ac67f24 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![CircleCI](https://circleci.com/gh/tammoippen/iso4217parse.svg?style=svg)](https://circleci.com/gh/tammoippen/iso4217parse) +[![CI](https://github.com/tammoippen/iso4217parse/actions/workflows/CI.yml/badge.svg?branch=master)](https://github.com/tammoippen/iso4217parse/actions/workflows/CI.yml) [![Coverage Status](https://coveralls.io/repos/github/tammoippen/iso4217parse/badge.svg?branch=master)](https://coveralls.io/github/tammoippen/iso4217parse?branch=master) [![Tested CPython Versions](https://img.shields.io/badge/cpython-3.9%2C%203.10%2C%203.11%2C%203.12%2C%203.13-brightgreen.svg)](https://img.shields.io/badge/cpython-3.9%2C%203.10%2C%203.11%2C%203.12%2C%203.13-brightgreen.svg) [![Tested PyPy Versions](https://img.shields.io/badge/pypy-3.9%2C%203.10-brightgreen.svg)](https://img.shields.io/badge/pypy-3.9%2C%203.10%2C%203.10-brightgreen.svg) @@ -11,12 +11,12 @@ Parse currencies (symbols and codes) from and to [ISO4217](https://en.wikipedia. Similar to [iso4217](https://github.com/spoqa/iso4217) package, but -- data is aquired by scraping wikipedia (see [below](#data-aquisition)) - this is repeatable and you stay on the most current data -- currency symbols are currated by hand - this allows some fuzzy currency matching +- data is acquired by scraping Wikipedia (see [below](#data-acquisition)) - this is repeatable and you stay on the most current data +- currency symbols are curated by hand - this allows some fuzzy currency matching - no download and parsing during install -- no external dependancies (`enum34`) +- no external dependencies (`enum34`) -When you want to _reuse_ the [_data.json_](https://github.com/tammoippen/iso4217parse/blob/master/iso4217parse/data.json) file for your projects, please leave a attribution note. I licence the file under (CC BY 4.0). +When you want to _reuse_ the [_data.json_](https://github.com/tammoippen/iso4217parse/blob/master/iso4217parse/data.json) file for your projects, please leave a attribution note. I license the file under (CC BY 4.0). Install: @@ -24,6 +24,8 @@ Install: pip install iso4217parse ``` +**(If you are required to use python version 2.7 or lower than 3.9, please use version [0.5.1](https://pypi.org/project/iso4217parse/0.5.1/))** + ## Documentation Each currency is modeled as a `collections.namedtuple`: @@ -208,7 +210,7 @@ Returns: List[Currency]: Currency objects for `symbol`; filter by country_code. ``` -**by_symbol_match:** Look for currency symbol occurence in input string: +**by_symbol_match:** Look for currency symbol occurrence in input string: ```python In [1]: import iso4217parse @@ -262,13 +264,17 @@ Returns: List[Currency]: Currency objects found in `value`; filter by country_code. ``` -## Data aquisition +## Contribution + +If you want to contribute + +## Data acquisition -Basic ISO4217 currency information is gathered from wikipedia: https://en.wikipedia.org/wiki/ISO_4217 . The tables are parsed with `gen_data.py` and stored in `iso4217parse/data.json`. This gives information for `alpha3`, `code_num`, `name`, `minor` and `countries`. The currency symbol information is hand gathered from: +Basic ISO4217 currency information is gathered from Wikipedia: [https://en.wikipedia.org/wiki/ISO_4217](https://en.wikipedia.org/wiki/ISO_4217) . The tables are parsed with `gen_data.py` and stored in `iso4217parse/data.json`. This gives information for `alpha3`, `code_num`, `name`, `minor` and `countries`. The currency symbol information is hand gathered from: -- individuel wikipedia pages, i.e. [EUR](https://en.wikipedia.org/wiki/Euro) has a `Denominations` -> `Symbol` section. -- http://www.iotafinance.com/en/ISO-4217-Currency-Codes.html -- http://www.xe.com/currency/ , i.e. [GBP](http://www.xe.com/currency/gbp-british-pound) has a `Currency Facts` -> `Symbol` section +- individual Wikipedia pages, i.e. [EUR](https://en.wikipedia.org/wiki/Euro) has a `Denominations` -> `Symbol` section. +- [http://www.iotafinance.com/en/ISO-4217-Currency-Codes.html](http://www.iotafinance.com/en/ISO-4217-Currency-Codes.html) +- [http://www.xe.com/currency/](http://www.xe.com/currency/) , i.e. [GBP](http://www.xe.com/currency/gbp-british-pound) has a `Currency Facts` -> `Symbol` section and stored in `iso4217parse/symbols.json`. Each currency can have multiple currency symbols - the first symbol in the list is the (opinionated) choice for the currency. From 96d28d15045a2c4a46f9659dfbb80d6cd59220d8 Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Wed, 6 Nov 2024 14:54:13 +0100 Subject: [PATCH 09/12] some contribution comments --- README.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ac67f24..40ea1d5 100644 --- a/README.md +++ b/README.md @@ -264,10 +264,6 @@ Returns: List[Currency]: Currency objects found in `value`; filter by country_code. ``` -## Contribution - -If you want to contribute - ## Data acquisition Basic ISO4217 currency information is gathered from Wikipedia: [https://en.wikipedia.org/wiki/ISO_4217](https://en.wikipedia.org/wiki/ISO_4217) . The tables are parsed with `gen_data.py` and stored in `iso4217parse/data.json`. This gives information for `alpha3`, `code_num`, `name`, `minor` and `countries`. The currency symbol information is hand gathered from: @@ -279,7 +275,25 @@ Basic ISO4217 currency information is gathered from Wikipedia: [https://en.wikip and stored in `iso4217parse/symbols.json`. Each currency can have multiple currency symbols - the first symbol in the list is the (opinionated) choice for the currency. -**Contribution Note**: Possible ways to contribute here: +## Contribution + +If you want to contribute, here are some ways you can help: - hand check symbols for currency code. -- automatic generation of the `iso4217parse/symbols.json` file. +- automatic generation of the `iso4217parse/symbols.json` file + +To setup the project for local development, be sure to use [poetry](https://python-poetry.org/) for the installation of the dependencies: + +```sh +# install dependencies +> poetry install + +# perform formatting +> make fmt + +# check style +> make check + +# run tests +> make test +``` From e01fc689ae6a30d36ce25b4756a32fba3753afb4 Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Thu, 7 Nov 2024 09:12:51 +0100 Subject: [PATCH 10/12] typing --- .gitignore | 2 +- Makefile | 2 +- iso4217parse/__init__.py | 121 ++++++++++++++++++++++---------------- iso4217parse/py.typed | 0 pyproject.toml | 9 +-- tests/test_by_alpha3.py | 5 +- tests/test_by_code_num.py | 5 +- tests/test_by_country.py | 3 - tests/test_by_symbol.py | 8 --- tests/test_parse.py | 5 +- 10 files changed, 80 insertions(+), 80 deletions(-) create mode 100644 iso4217parse/py.typed diff --git a/.gitignore b/.gitignore index 53d633c..e893b59 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ __pycache__/ # macOS .DS_Store - +.pypy_cache/ .ruff_cache/ dist/ pyproject.lock diff --git a/Makefile b/Makefile index aac582b..f056c1e 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ fmt: check: poetry run ruff format --check . poetry run ruff check . - # poetry run mypy src + poetry run mypy iso4217parse test: PYTHONDEVMODE=1 poetry run pytest -vvv -s diff --git a/iso4217parse/__init__.py b/iso4217parse/__init__.py index e3d3097..a381483 100644 --- a/iso4217parse/__init__.py +++ b/iso4217parse/__init__.py @@ -21,9 +21,11 @@ # THE SOFTWARE. from collections import defaultdict, namedtuple +from dataclasses import dataclass import importlib.resources import json import re +from typing import Optional __all__ = [ @@ -50,12 +52,21 @@ ) -_DATA = None -_SYMBOLS = None +@dataclass +class Data: + alpha3: dict[str, Currency] + code_num: dict[str, Currency] + symbol: dict[str, list[Currency]] + name: dict[str, Currency] + country: dict[str, list[Currency]] -def _data(): - """(Lazy)load index data structure for currences +_DATA: Optional[Data] = None +_SYMBOLS: Optional[list[tuple[str, str]]] = None + + +def _data() -> Data: + """(Lazy)load index data structure for currencies Load the `data.json` file (created with `gen_data.py`) and index by alpha3, code_num, symbol and country. @@ -65,48 +76,48 @@ def _data(): """ global _DATA if _DATA is None: - _DATA = {} with importlib.resources.open_text("iso4217parse", "data.json") as f: - _DATA["alpha3"] = {k: Currency(**v) for k, v in json.load(f).items()} + alpha3 = {k: Currency(**v) for k, v in json.load(f).items()} - _DATA["code_num"] = { - d.code_num: d for d in _DATA["alpha3"].values() if d.code_num is not None - } - _DATA["symbol"] = defaultdict(list) - for d in _DATA["alpha3"].values(): + code_num = {d.code_num: d for d in alpha3.values() if d.code_num is not None} + symbol: dict[str, list[Currency]] = defaultdict(list) + for d in alpha3.values(): for s in d.symbols: - _DATA["symbol"][s] += [d] + symbol[s] += [d] - for s, d in _DATA["symbol"].items(): - _DATA["symbol"][s] = sorted( - d, key=lambda d: 10000 if d.code_num is None else d.code_num + for s, ds in symbol.items(): + symbol[s] = sorted( + ds, key=lambda d: 10000 if d.code_num is None else d.code_num ) - _DATA["name"] = {} - for d in _DATA["alpha3"].values(): - if d.name in _DATA["name"]: + name = {} + for d in alpha3.values(): + if d.name in name: assert 'Duplicate name "{}"!'.format(d.name) - _DATA["name"][d.name] = d + name[d.name] = d - _DATA["country"] = defaultdict(list) - for d in _DATA["alpha3"].values(): + country: dict[str, list[Currency]] = defaultdict(list) + for d in alpha3.values(): for cc in d.countries: - _DATA["country"][cc] += [d] + country[cc] += [d] - for s, d in _DATA["country"].items(): - _DATA["country"][s] = sorted( - d, + for s, ds in country.items(): + country[s] = sorted( + ds, key=lambda d: ( int(d.symbols == []), # at least one symbol 10000 if d.code_num is None else d.code_num, # official first len(d.countries), # the fewer countries the more specific ), ) + _DATA = Data( + alpha3=alpha3, code_num=code_num, symbol=symbol, name=name, country=country + ) return _DATA -def _symbols(): +def _symbols() -> list[tuple[str, str]]: """(Lazy)load list of all supported symbols (sorted) Look into `_data()` for all currency symbols, then sort by length and @@ -117,15 +128,15 @@ def _symbols(): """ global _SYMBOLS if _SYMBOLS is None: - tmp = [(s, "symbol") for s in _data()["symbol"].keys()] - tmp += [(s, "alpha3") for s in _data()["alpha3"].keys()] - tmp += [(s.name, "name") for s in _data()["alpha3"].values()] + tmp = [(s, "symbol") for s in _data().symbol.keys()] + tmp += [(s, "alpha3") for s in _data().alpha3.keys()] + tmp += [(s, "name") for s in _data().name.keys()] _SYMBOLS = sorted(tmp, key=lambda s: (len(s[0]), ord(s[0][0])), reverse=True) return _SYMBOLS -def by_alpha3(code): +def by_alpha3(code) -> Optional[Currency]: """Get Currency for ISO4217 alpha3 code Parameters: @@ -134,10 +145,10 @@ def by_alpha3(code): Returns: Currency: Currency object for `code`, if available. """ - return _data()["alpha3"].get(code) + return _data().alpha3.get(code) -def by_code_num(code_num): +def by_code_num(code_num) -> Optional[Currency]: """Get Currency for ISO4217 numeric code Parameters: @@ -146,10 +157,10 @@ def by_code_num(code_num): Returns: Currency: return Currency object for `code_num`, if available. """ - return _data()["code_num"].get(code_num) + return _data().code_num.get(code_num) -def by_symbol(symbol, country_code=None): +def by_symbol(symbol, country_code=None) -> Optional[list[Currency]]: """Get list of possible currencies for symbol; filter by country_code Look for all currencies that use the `symbol`. If there are currencies used @@ -163,7 +174,7 @@ def by_symbol(symbol, country_code=None): Returns: List[Currency]: Currency objects for `symbol`; filter by country_code. """ - res = _data()["symbol"].get(symbol) + res = _data().symbol.get(symbol) if res: tmp_res = [] for d in res: @@ -174,9 +185,10 @@ def by_symbol(symbol, country_code=None): return tmp_res if country_code is None: return res + return None -def by_symbol_match(value, country_code=None): +def by_symbol_match(value, country_code=None) -> Optional[list[Currency]]: """Get list of possible currencies where the symbol is in value; filter by country_code (iso3166 alpha2 code) Look for first matching currency symbol in `value`. Filter similar to `by_symbol`. @@ -193,25 +205,31 @@ def by_symbol_match(value, country_code=None): Returns: List[Currency]: Currency objects found in `value`; filter by country_code. """ - res = None + res: Optional[list[Currency]] = None for symbol, group in _symbols(): symbol_pattern = re.escape(symbol) if re.search(rf"(^|\b|\d|\s){symbol_pattern}([^A-Z]|$)", value, re.I): if group == "symbol": res = by_symbol(symbol, country_code) if group == "alpha3": - res = [by_alpha3(symbol)] + curr = by_alpha3(symbol) + assert curr is not None + res = [curr] if group == "name": - res = [_data()["name"][symbol]] + curr = _data().name[symbol] + assert curr is not None + res = [curr] if res and country_code is not None: res = [ currency for currency in res if country_code in currency.countries ] + res = list(filter(None, res or [])) if res: return res + return None -def by_country(country_code): +def by_country(country_code) -> Optional[list[Currency]]: """Get all currencies used in country Parameters: @@ -221,10 +239,10 @@ def by_country(country_code): List[Currency]: Currency objects used in country. """ - return _data()["country"].get(country_code) + return _data().country.get(country_code) -def parse(v, country_code=None): +def parse(v, country_code=None) -> Optional[list[Currency]]: """Try parse `v` to currencies; filter by country_code If `v` is a number, try `by_code_num()`; otherwise try: @@ -256,16 +274,17 @@ def parse(v, country_code=None): return [res] # check by symbol - res = by_symbol(v, country_code) - if res: - return res + ress = by_symbol(v, country_code) + if ress: + return ress # check by country code - res = by_country(v) - if res: - return res + ress = by_country(v) + if ress: + return ress # more or less fuzzy match by symbol - res = by_symbol_match(v, country_code) - if res: - return res + ress = by_symbol_match(v, country_code) + if ress: + return ress + return None diff --git a/iso4217parse/py.typed b/iso4217parse/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index c876362..8177d9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,10 +31,11 @@ classifiers=[ [tool.poetry.dependencies] python = "^3.9" -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] coveralls = "*" -iso3166 = "^2" -pytest = "^8" -pytest-cov = "^6" +iso3166 = "*" +mypy = "*" +pytest = "*" +pytest-cov = "*" ruff = "*" diff --git a/tests/test_by_alpha3.py b/tests/test_by_alpha3.py index 046c02f..e76df5a 100644 --- a/tests/test_by_alpha3.py +++ b/tests/test_by_alpha3.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division, print_function, unicode_literals - import iso4217parse @@ -11,7 +8,7 @@ def test_invalid(): def test_all_currencies(): - for code in iso4217parse._data()["alpha3"].keys(): + for code in iso4217parse._data().alpha3.keys(): assert isinstance(iso4217parse.by_alpha3(code), iso4217parse.Currency) diff --git a/tests/test_by_code_num.py b/tests/test_by_code_num.py index 267a44b..7b56010 100644 --- a/tests/test_by_code_num.py +++ b/tests/test_by_code_num.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division, print_function, unicode_literals - import iso4217parse @@ -11,7 +8,7 @@ def test_invalid(): def test_all_currencies(): - for code in iso4217parse._data()["code_num"].keys(): + for code in iso4217parse._data().code_num.keys(): assert isinstance(iso4217parse.by_code_num(code), iso4217parse.Currency) diff --git a/tests/test_by_country.py b/tests/test_by_country.py index 37aafec..c223601 100644 --- a/tests/test_by_country.py +++ b/tests/test_by_country.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division, print_function, unicode_literals - import iso3166 import iso4217parse diff --git a/tests/test_by_symbol.py b/tests/test_by_symbol.py index 172d2f4..2fece2c 100644 --- a/tests/test_by_symbol.py +++ b/tests/test_by_symbol.py @@ -1,11 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - import pytest import iso4217parse diff --git a/tests/test_parse.py b/tests/test_parse.py index 56a3e38..c91317e 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division, print_function, unicode_literals - import pytest import iso4217parse as iso4217 @@ -13,7 +10,7 @@ def test_invalid(): def test_examples_code(): - for code, exp in iso4217._data()["alpha3"].items(): + for code, exp in iso4217._data().alpha3.items(): assert [exp] == iso4217.parse(code) From d5011af1bc37e0def168d7fbe19f76099f6f5091 Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Thu, 7 Nov 2024 09:19:17 +0100 Subject: [PATCH 11/12] fmt --- tests/test_by_symbol.py | 45 ++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/tests/test_by_symbol.py b/tests/test_by_symbol.py index 2fece2c..8a97bfb 100644 --- a/tests/test_by_symbol.py +++ b/tests/test_by_symbol.py @@ -37,16 +37,16 @@ def test_by_symbol_match_filters_country_code( @pytest.mark.parametrize( - 'text, expected_alpha3', + "text, expected_alpha3", ( # symbol should be lowercase - ('lek', 'ALL'), - ('Lek', 'ALL'), - ('LEK', 'ALL'), + ("lek", "ALL"), + ("Lek", "ALL"), + ("LEK", "ALL"), # symbol should be uppercase - ('DH', 'AED'), - ('Dh', 'AED'), - ('dh', 'AED'), + ("DH", "AED"), + ("Dh", "AED"), + ("dh", "AED"), ), ) def test_parse_by_symbol_value_is_case_insensitive(text, expected_alpha3): @@ -56,28 +56,31 @@ def test_parse_by_symbol_value_is_case_insensitive(text, expected_alpha3): @pytest.mark.parametrize( - 'text, ambiguous_alpha3, wanted_alpha3', + "text, ambiguous_alpha3, wanted_alpha3", ( # ambiguous words + symbol - ('cost $100', 'WST', 'USD'), # st$ => WST + ("cost $100", "WST", "USD"), # st$ => WST # only letters found in words - ('durée 100', 'SZL', None), # e => SWL / no currency - ('maximum 100', 'MRO', None), # um => MRO / no currency - ('flowers', 'NPR', None), # Re => NPR / no currency - ('flowers', 'LKR', None), # Re => LKR / no currency - ('flowers', 'PKR', None), # Re => PRK / no currency - ('amount : 100 currency : Ks', 'NPR', 'MMK'), # Re => NPR / Ks is MMK - ('yes: 100l', 'SOS', 'ALL'), # s gives SOS / l is ALL or LSL + ("durée 100", "SZL", None), # e => SWL / no currency + ("maximum 100", "MRO", None), # um => MRO / no currency + ("flowers", "NPR", None), # Re => NPR / no currency + ("flowers", "LKR", None), # Re => LKR / no currency + ("flowers", "PKR", None), # Re => PRK / no currency + ("amount : 100 currency : Ks", "NPR", "MMK"), # Re => NPR / Ks is MMK + ("yes: 100l", "SOS", "ALL"), # s gives SOS / l is ALL or LSL # alpha 3 codes found in words - ('course', 'COU', None), # COU => COU - ('finance', 'ANG', None), # ANG => ANG + ("course", "COU", None), # COU => COU + ("finance", "ANG", None), # ANG => ANG ), ) def test_parse_by_symbol_value_disambiguation(text, ambiguous_alpha3, wanted_alpha3): - assert iso4217parse.by_alpha3(ambiguous_alpha3) not in (iso4217parse.by_symbol_match(text) or []) + assert iso4217parse.by_alpha3(ambiguous_alpha3) not in ( + iso4217parse.by_symbol_match(text) or [] + ) if wanted_alpha3: - assert iso4217parse.by_alpha3(wanted_alpha3) in iso4217parse.by_symbol_match(text) + assert iso4217parse.by_alpha3(wanted_alpha3) in iso4217parse.by_symbol_match( + text + ) else: assert iso4217parse.by_symbol_match(text) is None - From f9c794c8b56035292fb69a9542a3ae2d87a6e1f2 Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Thu, 7 Nov 2024 09:25:53 +0100 Subject: [PATCH 12/12] cache compiled re patterns --- iso4217parse/__init__.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/iso4217parse/__init__.py b/iso4217parse/__init__.py index a381483..bcddc2c 100644 --- a/iso4217parse/__init__.py +++ b/iso4217parse/__init__.py @@ -22,6 +22,7 @@ from collections import defaultdict, namedtuple from dataclasses import dataclass +from functools import lru_cache import importlib.resources import json import re @@ -136,7 +137,7 @@ def _symbols() -> list[tuple[str, str]]: return _SYMBOLS -def by_alpha3(code) -> Optional[Currency]: +def by_alpha3(code: str) -> Optional[Currency]: """Get Currency for ISO4217 alpha3 code Parameters: @@ -148,7 +149,7 @@ def by_alpha3(code) -> Optional[Currency]: return _data().alpha3.get(code) -def by_code_num(code_num) -> Optional[Currency]: +def by_code_num(code_num: str) -> Optional[Currency]: """Get Currency for ISO4217 numeric code Parameters: @@ -160,7 +161,9 @@ def by_code_num(code_num) -> Optional[Currency]: return _data().code_num.get(code_num) -def by_symbol(symbol, country_code=None) -> Optional[list[Currency]]: +def by_symbol( + symbol: str, country_code: Optional[str] = None +) -> Optional[list[Currency]]: """Get list of possible currencies for symbol; filter by country_code Look for all currencies that use the `symbol`. If there are currencies used @@ -188,7 +191,15 @@ def by_symbol(symbol, country_code=None) -> Optional[list[Currency]]: return None -def by_symbol_match(value, country_code=None) -> Optional[list[Currency]]: +@lru_cache(maxsize=1024) +def _symbol_pattern(symbol: str) -> re.Pattern: + symbol_pattern = re.escape(symbol) + return re.compile(rf"(^|\b|\d|\s){symbol_pattern}([^A-Z]|$)", re.I) + + +def by_symbol_match( + value: str, country_code: Optional[str] = None +) -> Optional[list[Currency]]: """Get list of possible currencies where the symbol is in value; filter by country_code (iso3166 alpha2 code) Look for first matching currency symbol in `value`. Filter similar to `by_symbol`. @@ -206,9 +217,10 @@ def by_symbol_match(value, country_code=None) -> Optional[list[Currency]]: List[Currency]: Currency objects found in `value`; filter by country_code. """ res: Optional[list[Currency]] = None + print(len(_symbols())) for symbol, group in _symbols(): - symbol_pattern = re.escape(symbol) - if re.search(rf"(^|\b|\d|\s){symbol_pattern}([^A-Z]|$)", value, re.I): + symbol_pattern = _symbol_pattern(symbol) + if symbol_pattern.search(value): if group == "symbol": res = by_symbol(symbol, country_code) if group == "alpha3": @@ -229,7 +241,7 @@ def by_symbol_match(value, country_code=None) -> Optional[list[Currency]]: return None -def by_country(country_code) -> Optional[list[Currency]]: +def by_country(country_code: str) -> Optional[list[Currency]]: """Get all currencies used in country Parameters: @@ -242,7 +254,7 @@ def by_country(country_code) -> Optional[list[Currency]]: return _data().country.get(country_code) -def parse(v, country_code=None) -> Optional[list[Currency]]: +def parse(v: str, country_code: Optional[str] = None) -> Optional[list[Currency]]: """Try parse `v` to currencies; filter by country_code If `v` is a number, try `by_code_num()`; otherwise try: