From 316417bfecf8ab643a07583beccef1fda7a2ba2f Mon Sep 17 00:00:00 2001 From: Damien Date: Sat, 1 Jan 2022 18:25:46 +1100 Subject: [PATCH 1/9] First working conversion to Flox --- main.py | 4 +- plugin/currency_converter.py | 105 +++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 plugin/currency_converter.py diff --git a/main.py b/main.py index 06f248e..06a264f 100644 --- a/main.py +++ b/main.py @@ -6,7 +6,7 @@ sys.path.append(os.path.join(parent_folder_path, 'lib')) sys.path.append(os.path.join(parent_folder_path, 'plugin')) -from plugin import Main +from plugin.currency_converter import Currency if __name__ == "__main__": - Main() \ No newline at end of file + Currency() \ No newline at end of file diff --git a/plugin/currency_converter.py b/plugin/currency_converter.py new file mode 100644 index 0000000..0c15e06 --- /dev/null +++ b/plugin/currency_converter.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- + +import copy +import plugin.utils +import decimal + +from typing import List +from flox import Flox + +class Currency(Flox): + currencies = [ + "AUD", + "BGN", + "BRL", + "CAD", + "CHF", + "CNY", + "CZK", + "DKK", + "GBP", + "HKD", + "HRK", + "HUF", + "IDR", + "ILS", + "INR", + "ISK", + "JPY", + "KRW", + "MXN", + "MYR", + "NOK", + "NZD", + "PHP", + "PLN", + "RON", + "RUB", + "SEK", + "SGD", + "THB", + "TRY", + "USD", + "ZAR", + "EUR", + ] + + def query(self, query): + q = query.strip() + args = q.split(" ") + if len(args) == 3: + # Check codes are three letters + if len(args[1]) != 3 or len(args[2]) != 3: + self.add_item( + title="Please enter three character currency codes" + ) + + # Check first argument is valid currency code + elif len(args[1]) == 3 and args[1].upper() not in self.currencies: + self.add_item( + title="Error - {} not a valid currency").format(args[1].upper() + ) + # Check second argument is valid currency code + elif len(args[2]) == 3 and args[2].upper() not in self.currencies: + self.add_item( + title="Error - {} not a valid currency").format(args[2].upper() + ) + # Do the conversion + else: + # If source and dest currencies the same just return entered amount + if args[1].upper() == args[2].upper(): + self.add_item( + title="{} {} = {} {}".format( + args[0], args[1].upper(), args[0], args[2].upper() + ) + ) + else: + try: + # First strip any commas from the amount + args[0] = args[0].replace(",", "") + ratesxml_returncode = plugin.utils.getrates_xml() + ratedict = plugin.utils.populate_rates("eurofxref-daily.xml") + conv = plugin.utils.currconv( + ratedict, args[1], args[2], args[0] + ) + # decimal.getcontext().prec = conv[2] + self.add_item( + title = (f"{args[0]} {args[1].upper()} = " + f"{round(decimal.Decimal(conv[1]), conv[2])} " + f"{args[2].upper()} " + f"(1 {args[1].upper()} = " + f"{round(decimal.Decimal(conv[1]) / decimal.Decimal(args[0]),conv[2],)} " + f"{args[2].upper()})"), + subtitle = f"Rates date : {conv[0]}") + # Show exceptions (for debugging as much as anything else) + except Exception as e: + self.add_item("Error - {}").format(repr(e)) + # Always show the usage while there isn't a valid query + else: + self.add_item( + title = "Currency Converter 2.0", + subtitle = f" ", + ) + +if __name__ == "__main__": + Currency() \ No newline at end of file From c2bfc33aa7a7e13b7f0dea434bb989081f355915 Mon Sep 17 00:00:00 2001 From: Damien Date: Tue, 29 Mar 2022 19:56:32 +1100 Subject: [PATCH 2/9] Refactor from old template to Flox. Add thousands separator. --- LICENSE | 2 +- commands.py | 98 ------------------------- plugin.json | 2 +- plugin/__init__.py | 24 ------ plugin/currency_converter.py | 135 ++++++++++++++++++++-------------- plugin/extensions.py | 10 --- plugin/settings.py | 42 ----------- plugin/templates.py | 18 ----- plugin/translation.py | 19 +++++ plugin/ui.py | 138 ----------------------------------- requirements-dev.txt | 6 +- requirements.txt | 1 + test.py | 8 -- 13 files changed, 108 insertions(+), 395 deletions(-) delete mode 100644 commands.py delete mode 100644 plugin/__init__.py delete mode 100644 plugin/extensions.py delete mode 100644 plugin/settings.py delete mode 100644 plugin/templates.py create mode 100644 plugin/translation.py delete mode 100644 plugin/ui.py delete mode 100644 test.py diff --git a/LICENSE b/LICENSE index 70caa44..af8543c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 deefrawley +Copyright (c) 2022 deefrawley 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/commands.py b/commands.py deleted file mode 100644 index 9b15e67..0000000 --- a/commands.py +++ /dev/null @@ -1,98 +0,0 @@ -# -*- coding: utf-8 -*- - -import json -import os - -import click - -from plugin import ( - ICON_PATH, - PLUGIN_ACTION_KEYWORD, - PLUGIN_AUTHOR, - PLUGIN_EXECUTE_FILENAME, - PLUGIN_ID, - PLUGIN_PROGRAM_LANG, - PLUGIN_URL, - __long_description__, - __package_name__, - __short_description__, - __version__, - basedir, -) - - -@click.group() -def translate(): - """Translation and localization commands.""" - ... - - -@translate.command() -@click.argument("locale") -def init(locale): - """Initialize a new language.""" - - if os.system("pybabel extract -F babel.cfg -k _l -o messages.pot ."): - raise RuntimeError("extract command failed") - if os.system("pybabel init -i messages.pot -d plugin/translations -l " + locale): - raise RuntimeError("init command failed") - os.remove("messages.pot") - - click.echo("Done.") - - -@translate.command() -def update(): - """Update all languages.""" - if os.system("pybabel extract -F babel.cfg -k _l -o messages.pot ."): - raise RuntimeError("extract command failed") - if os.system("pybabel update -i messages.pot -d plugin/translations"): - raise RuntimeError("update command failed") - os.remove("messages.pot") - - click.echo("Done.") - - -@translate.command() -def compile(): - """Compile all languages.""" - if os.system("pybabel compile -d plugin/translations"): - raise RuntimeError("compile command failed") - - click.echo("Done.") - - -@click.group() -def plugin(): - """Translation and localization commands.""" - ... - - -@plugin.command() -def gen_plugin_info(): - """Auto generate the `plugin.json` file for Flow""" - - plugin_infos = { - "ID": PLUGIN_ID, - "ActionKeyword": PLUGIN_ACTION_KEYWORD, - "Name": __package_name__.title(), - "Description": __short_description__, - "Author": PLUGIN_AUTHOR, - "Version": __version__, - "Language": PLUGIN_PROGRAM_LANG, - "Website": PLUGIN_URL, - "IcoPath": ICON_PATH, - "ExecuteFileName": PLUGIN_EXECUTE_FILENAME, - } - - json_path = os.path.join(basedir, "plugin.json") - with open(json_path, "w") as f: - json.dump(plugin_infos, f, indent=" " * 4) - - click.echo("Done.") - - -cli = click.CommandCollection(sources=[plugin, translate]) - -if __name__ == "__main__": - cli() \ No newline at end of file diff --git a/plugin.json b/plugin.json index 53c9102..b125286 100644 --- a/plugin.json +++ b/plugin.json @@ -4,7 +4,7 @@ "Name": "Currency Converter", "Description": "Currency converter using the euro and rates at https://www.ecb.europa.eu/", "Author": "deefrawley", - "Version": "1.2.2", + "Version": "2.0.0", "Language": "python", "Website": "https://github.com/deefrawley/Flow.Launcher.Plugin.Currency", "IcoPath": "assets/favicon.ico", diff --git a/plugin/__init__.py b/plugin/__init__.py deleted file mode 100644 index a0f0f1a..0000000 --- a/plugin/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Currency Converter -===== -Uses the European Central Bank to convert currencies. -""" - - -from plugin.settings import ( - GITHUB_USERNAME, - ICON_PATH, - PLUGIN_ACTION_KEYWORD, - PLUGIN_AUTHOR, - PLUGIN_EXECUTE_FILENAME, - PLUGIN_ID, - PLUGIN_PROGRAM_LANG, - PLUGIN_URL, - __long_description__, - __package_name__, - __short_description__, - __version__, - basedir, -) -from plugin.ui import Main \ No newline at end of file diff --git a/plugin/currency_converter.py b/plugin/currency_converter.py index 0c15e06..1e5c105 100644 --- a/plugin/currency_converter.py +++ b/plugin/currency_converter.py @@ -1,48 +1,51 @@ # -*- coding: utf-8 -*- - -import copy +import textwrap import plugin.utils import decimal +import locale -from typing import List +from plugin.translation import _ from flox import Flox + class Currency(Flox): + locale.setlocale(locale.LC_ALL, "") + # TODO - save list to settings and update from each XML download and just use this list as a first time default currencies = [ - "AUD", - "BGN", - "BRL", - "CAD", - "CHF", - "CNY", - "CZK", - "DKK", - "GBP", - "HKD", - "HRK", - "HUF", - "IDR", - "ILS", - "INR", - "ISK", - "JPY", - "KRW", - "MXN", - "MYR", - "NOK", - "NZD", - "PHP", - "PLN", - "RON", - "RUB", - "SEK", - "SGD", - "THB", - "TRY", - "USD", - "ZAR", - "EUR", - ] + "AUD", + "BGN", + "BRL", + "CAD", + "CHF", + "CNY", + "CZK", + "DKK", + "GBP", + "HKD", + "HRK", + "HUF", + "IDR", + "ILS", + "INR", + "ISK", + "JPY", + "KRW", + "MXN", + "MYR", + "NOK", + "NZD", + "PHP", + "PLN", + "RON", + "RUB", + "SEK", + "SGD", + "THB", + "TRY", + "USD", + "ZAR", + "EUR", + ] def query(self, query): q = query.strip() @@ -50,19 +53,17 @@ def query(self, query): if len(args) == 3: # Check codes are three letters if len(args[1]) != 3 or len(args[2]) != 3: - self.add_item( - title="Please enter three character currency codes" - ) + self.add_item(title=_("Please enter three character currency codes")) # Check first argument is valid currency code elif len(args[1]) == 3 and args[1].upper() not in self.currencies: - self.add_item( - title="Error - {} not a valid currency").format(args[1].upper() + self.add_item(title=_("Error - {} not a valid currency")).format( + args[1].upper() ) # Check second argument is valid currency code elif len(args[2]) == 3 and args[2].upper() not in self.currencies: - self.add_item( - title="Error - {} not a valid currency").format(args[2].upper() + self.add_item(title=_("Error - {} not a valid currency")).format( + args[2].upper() ) # Do the conversion else: @@ -77,6 +78,7 @@ def query(self, query): try: # First strip any commas from the amount args[0] = args[0].replace(",", "") + # TODO Handle non 200 return code ratesxml_returncode = plugin.utils.getrates_xml() ratedict = plugin.utils.populate_rates("eurofxref-daily.xml") conv = plugin.utils.currconv( @@ -84,22 +86,47 @@ def query(self, query): ) # decimal.getcontext().prec = conv[2] self.add_item( - title = (f"{args[0]} {args[1].upper()} = " - f"{round(decimal.Decimal(conv[1]), conv[2])} " - f"{args[2].upper()} " - f"(1 {args[1].upper()} = " - f"{round(decimal.Decimal(conv[1]) / decimal.Decimal(args[0]),conv[2],)} " - f"{args[2].upper()})"), - subtitle = f"Rates date : {conv[0]}") + title=( + f"{locale.format_string('%.3f', float(args[0]), grouping=True)} {args[1].upper()} = " + f"{locale.format_string('%.3f', round(decimal.Decimal(conv[1]), conv[2]), grouping=True)} " + f"{args[2].upper()} " + f"(1 {args[1].upper()} = " + f"{round(decimal.Decimal(conv[1]) / decimal.Decimal(args[0]),conv[2],)} " + f"{args[2].upper()})" + ), + subtitle=f"Rates date : {conv[0]}", + ) # Show exceptions (for debugging as much as anything else) except Exception as e: self.add_item("Error - {}").format(repr(e)) # Always show the usage while there isn't a valid query else: self.add_item( - title = "Currency Converter 2.0", - subtitle = f" ", + title=_(" "), + subtitle=_( + "There will be a short delay if the currency rates file needs to be downloaded" + ), ) + title = _("Available currencies:") + subtitle = ", ".join(self.currencies) + lines = textwrap.wrap(subtitle, 110) + if len(lines) > 1: + self.add_item( + title=(title), + subtitle=(lines[0]), + ) + for line in range(1, len(lines)): + self.add_item(title="", subtitle=(lines[line]), icon="garbage") + else: + self.add_item( + (title), + (subtitle), + ) + # self.add_item( + # title=_("Currencies available:"), + # subtitle=_(f"{', '.join(self.currencies)}"), + # ) + if __name__ == "__main__": - Currency() \ No newline at end of file + Currency() diff --git a/plugin/extensions.py b/plugin/extensions.py deleted file mode 100644 index 5d07e27..0000000 --- a/plugin/extensions.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- - -import gettext - -from plugin.settings import LOCAL - -# localization -translation = gettext.translation("messages", "plugin/translations/", languages=[LOCAL]) - -_ = translation.gettext \ No newline at end of file diff --git a/plugin/settings.py b/plugin/settings.py deleted file mode 100644 index 29ca47a..0000000 --- a/plugin/settings.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- - -import os -from dotenv import load_dotenv - -basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) - -dotenv_path = os.path.join(basedir, ".env") -if os.path.exists(dotenv_path): - load_dotenv(dotenv_path) - - -# The default value can work, if no user config. -LOCAL = os.getenv("local", "en") - - -ICON_PATH = "assets/favicon.ico" - -# the information of package -__package_name__ = "Flow.Launcher.Plugin.Currency" -__version__ = "1.0.0" -__short_description__ = ( - "Currency converter using the euro and rates at https://www.ecb.europa.eu/" -) -GITHUB_USERNAME = "deefrawley" - - -readme_path = os.path.join(basedir, "README.md") -try: - __long_description__ = open(readme_path, "r").read() -except: - __long_description__ = __short_description__ - - -# other information -PLUGIN_ID = "18892b78-63ac-43ab-a278-59a5a2866dd8" -ICON_PATH = "assets/favicon.ico" -PLUGIN_ACTION_KEYWORD = "cc" -PLUGIN_AUTHOR = "deefrawley" -PLUGIN_PROGRAM_LANG = "python" -PLUGIN_URL = f"https://github.com/{GITHUB_USERNAME}/{__package_name__}" -PLUGIN_EXECUTE_FILENAME = "main.py" diff --git a/plugin/templates.py b/plugin/templates.py deleted file mode 100644 index 670cf02..0000000 --- a/plugin/templates.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- - - -from plugin.settings import ICON_PATH - - -RESULT_TEMPLATE = { - "Title": "", - "SubTitle": "", - "IcoPath": ICON_PATH, -} - -ACTION_TEMPLATE = { - "JsonRPCAction": { - "method": "", - "parameters": [], - } -} \ No newline at end of file diff --git a/plugin/translation.py b/plugin/translation.py new file mode 100644 index 0000000..d6f5925 --- /dev/null +++ b/plugin/translation.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +import os +import gettext +from dotenv import load_dotenv + +# TODO - Move language option to Flow setting and remove need for dotenv + +LOCAL = os.getenv("local", "en") + +basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) + +dotenv_path = os.path.join(basedir, ".env") +if os.path.exists(dotenv_path): + load_dotenv(dotenv_path) + +# localization +translation = gettext.translation("messages", "plugin/translations/", languages=[LOCAL]) + +_ = translation.gettext diff --git a/plugin/ui.py b/plugin/ui.py deleted file mode 100644 index 8ed3cc8..0000000 --- a/plugin/ui.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- coding: utf-8 -*- - -import copy -import plugin.utils -import decimal - -from typing import List -from plugin.templates import RESULT_TEMPLATE, ACTION_TEMPLATE -from flowlauncher import FlowLauncher -from plugin.extensions import _ - - -class Main(FlowLauncher): - messages_queue = [] - - def sendNormalMess(self, title: str, subtitle: str): - message = copy.deepcopy(RESULT_TEMPLATE) - message["Title"] = title - message["SubTitle"] = subtitle - - self.messages_queue.append(message) - - def sendActionMess(self, title: str, subtitle: str, method: str, value: List): - # information - message = copy.deepcopy(RESULT_TEMPLATE) - message["Title"] = title - message["SubTitle"] = subtitle - - # action - action = copy.deepcopy(ACTION_TEMPLATE) - action["JsonRPCAction"]["method"] = method - action["JsonRPCAction"]["parameters"] = value - message.update(action) - - self.messages_queue.append(message) - - def query(self, param: str) -> List[dict]: - q = param.strip() - args = q.split(" ") - - currencies = [ - "AUD", - "BGN", - "BRL", - "CAD", - "CHF", - "CNY", - "CZK", - "DKK", - "GBP", - "HKD", - "HRK", - "HUF", - "IDR", - "ILS", - "INR", - "ISK", - "JPY", - "KRW", - "MXN", - "MYR", - "NOK", - "NZD", - "PHP", - "PLN", - "RON", - "RUB", - "SEK", - "SGD", - "THB", - "TRY", - "USD", - "ZAR", - "EUR", - ] - if len(args) == 3: - # Check codes are three letters - if len(args[1]) != 3 or len(args[2]) != 3: - self.sendNormalMess( - _("Please enter three character currency codes"), "" - ) - - # Check first argument is valid currency code - elif len(args[1]) == 3 and args[1].upper() not in currencies: - self.sendNormalMess( - _("Error - {} not a valid currency").format(args[1].upper()), "" - ) - # Check second argument is valid currency code - elif len(args[2]) == 3 and args[2].upper() not in currencies: - self.sendNormalMess( - _("Error - {} not a valid currency").format(args[2].upper()), "" - ) - # Do the conversion - else: - # If source and dest currencies the same just return entered amount - if args[1].upper() == args[2].upper(): - self.sendNormalMess( - "{} {} = {} {}".format( - args[0], args[1].upper(), args[0], args[2].upper() - ), - "", - ) - else: - try: - # First strip any commas from the amount - args[0] = args[0].replace(",", "") - ratesxml_returncode = plugin.utils.getrates_xml() - ratedict = plugin.utils.populate_rates("eurofxref-daily.xml") - conv = plugin.utils.currconv( - ratedict, args[1], args[2], args[0] - ) - # decimal.getcontext().prec = conv[2] - self.sendNormalMess( - "{} {} = {} {} (1 {} = {} {})".format( - args[0], - args[1].upper(), - round(decimal.Decimal(conv[1]), conv[2]), - args[2].upper(), - args[1].upper(), - round( - decimal.Decimal(conv[1]) / decimal.Decimal(args[0]), - conv[2], - ), - args[2].upper(), - ), - _("Rates date : {}").format(conv[0]), - ) - # Show exceptions (for debugging as much as anything else) - except Exception as e: - self.sendNormalMess(_("Error - {}").format(repr(e)), "") - # Always show the usage while there isn't a valid query - else: - self.sendNormalMess( - _("Currency Converter"), - _(" "), - ) - - return self.messages_queue diff --git a/requirements-dev.txt b/requirements-dev.txt index 123fc3e..6ee4979 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,6 @@ -click +flowlauncher +flox +python-dotenv +requests +typing babel \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index cb95c44..6e3e73f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ flowlauncher +flox python-dotenv requests typing \ No newline at end of file diff --git a/test.py b/test.py deleted file mode 100644 index 174921b..0000000 --- a/test.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- - - -from plugin import Main - -if __name__ == "__main__": - r = Main().query("cc 46000 GBP AUD") - print(r) \ No newline at end of file From 01323c472c31f7d0a1fc5f9cd50ec628571922d2 Mon Sep 17 00:00:00 2001 From: Damien Date: Tue, 29 Mar 2022 21:51:05 +1100 Subject: [PATCH 3/9] Include settings, logging, excpetion handling and reintroduce translations helper script --- SettingsTemplate.yml | 36 ++++++++ plugin/currency_converter.py | 152 +++++++++++++++++++++++++++++----- plugin/utils.py | 154 ----------------------------------- requirements-dev.txt | 3 +- translations_helper.py | 53 ++++++++++++ 5 files changed, 222 insertions(+), 176 deletions(-) create mode 100644 SettingsTemplate.yml delete mode 100644 plugin/utils.py create mode 100644 translations_helper.py diff --git a/SettingsTemplate.yml b/SettingsTemplate.yml new file mode 100644 index 0000000..0de42f8 --- /dev/null +++ b/SettingsTemplate.yml @@ -0,0 +1,36 @@ +body: + - type: textBlock + attributes: + name: description + description: > + Choose the age, in hours, of the downloaded currency rates file before the plugin will download a new version. + The reference rates are usually updated around 16:00 CET on every working day, except on TARGET closing days. + See https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html for more details. + - type: dropdown + attributes: + name: max_age + label: "File age:" + defaultValue: 3 + options: + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + - 13 + - 14 + - 15 + - 16 + - 17 + - 18 + - 19 + - 20 + - 21 + - 22 + - 23 diff --git a/plugin/currency_converter.py b/plugin/currency_converter.py index 1e5c105..8705abd 100644 --- a/plugin/currency_converter.py +++ b/plugin/currency_converter.py @@ -1,8 +1,14 @@ # -*- coding: utf-8 -*- import textwrap -import plugin.utils + +# import plugin.utils import decimal import locale +import requests +import decimal +import xml.etree.ElementTree as ET +import os +import datetime from plugin.translation import _ from flox import Flox @@ -11,7 +17,7 @@ class Currency(Flox): locale.setlocale(locale.LC_ALL, "") # TODO - save list to settings and update from each XML download and just use this list as a first time default - currencies = [ + CURRENCIES = [ "AUD", "BGN", "BRL", @@ -47,6 +53,11 @@ class Currency(Flox): "EUR", ] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.max_age = self.settings.get("max_age") + self.logger_level("info") + def query(self, query): q = query.strip() args = q.split(" ") @@ -56,12 +67,12 @@ def query(self, query): self.add_item(title=_("Please enter three character currency codes")) # Check first argument is valid currency code - elif len(args[1]) == 3 and args[1].upper() not in self.currencies: + elif len(args[1]) == 3 and args[1].upper() not in self.CURRENCIES: self.add_item(title=_("Error - {} not a valid currency")).format( args[1].upper() ) # Check second argument is valid currency code - elif len(args[2]) == 3 and args[2].upper() not in self.currencies: + elif len(args[2]) == 3 and args[2].upper() not in self.CURRENCIES: self.add_item(title=_("Error - {} not a valid currency")).format( args[2].upper() ) @@ -75,15 +86,13 @@ def query(self, query): ) ) else: - try: - # First strip any commas from the amount - args[0] = args[0].replace(",", "") - # TODO Handle non 200 return code - ratesxml_returncode = plugin.utils.getrates_xml() - ratedict = plugin.utils.populate_rates("eurofxref-daily.xml") - conv = plugin.utils.currconv( - ratedict, args[1], args[2], args[0] - ) + + # First strip any commas from the amount + args[0] = args[0].replace(",", "") + ratesxml_returncode = self.getrates_xml(self.max_age) + if ratesxml_returncode == 200: + ratedict = self.populate_rates("eurofxref-daily.xml") + conv = self.currconv(ratedict, args[1], args[2], args[0]) # decimal.getcontext().prec = conv[2] self.add_item( title=( @@ -96,9 +105,14 @@ def query(self, query): ), subtitle=f"Rates date : {conv[0]}", ) - # Show exceptions (for debugging as much as anything else) - except Exception as e: - self.add_item("Error - {}").format(repr(e)) + else: + self.add_item( + title=_("Couldn't download the rates file"), + subtitle=_( + f"{ratesxml_returncode} - check log for more details" + ), + ) + # Always show the usage while there isn't a valid query else: self.add_item( @@ -108,7 +122,7 @@ def query(self, query): ), ) title = _("Available currencies:") - subtitle = ", ".join(self.currencies) + subtitle = ", ".join(self.CURRENCIES) lines = textwrap.wrap(subtitle, 110) if len(lines) > 1: self.add_item( @@ -122,10 +136,106 @@ def query(self, query): (title), (subtitle), ) - # self.add_item( - # title=_("Currencies available:"), - # subtitle=_(f"{', '.join(self.currencies)}"), - # ) + + def populate_rates(self, xml): + tree = ET.parse(xml) + root = tree.getroot() + rates = {} + for root_Cube in root.findall( + "{http://www.ecb.int/vocabulary/2002-08-01/eurofxref}Cube" + ): + for time_Cube in root_Cube.findall( + "{http://www.ecb.int/vocabulary/2002-08-01/eurofxref}Cube" + ): + rates.update({"date": "{}".format(time_Cube.attrib["time"])}) + for currency_Cube in time_Cube.findall( + "{http://www.ecb.int/vocabulary/2002-08-01/eurofxref}Cube" + ): + rates.update( + { + "{}".format(currency_Cube.attrib["currency"]): "{}".format( + currency_Cube.attrib["rate"] + ) + } + ) + return rates + + def getrates_xml(self, max_age): + + xmlfile = "eurofxref-daily.xml" + exists = os.path.isfile(xmlfile) + getnewfile = True + if exists: + current = datetime.datetime.now() + t = os.path.getmtime(xmlfile) + file = datetime.datetime.fromtimestamp(t) + if (current - file) > datetime.timedelta(hours=int(max_age)): + getnewfile = True + else: + getnewfile = False + if getnewfile: + try: + URL = "http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml" + r = requests.get(URL) + with open(xmlfile, "wb") as file: + file.write(r.content) + self.logger.info(f"Download rates file returned {r.status_code}") + return r.status_code + except requests.exceptions.HTTPError as e: + self.logger.error(f"HTTP Error - {repr(e)}") + return _("HTTP Error") + except requests.exceptions.ConnectionError as e: + self.logger.error(f"Connection Error - {repr(e)}") + return _("Connection Error") + except requests.exceptions.RequestException as e: + self.logger.error(f"Unspecified Download Error - {repr(e)}") + return _("Unspecifed Download Error") + else: + return 200 + + def currconv(self, rates, sourcecurr, destcurr, amount): + converted = [] + + # Change the decimal precision to match the number of digits in the amount + if "." in amount: + dec_prec = len(amount.split(".")[1]) + # Default to precision of 3 decimal places + else: + dec_prec = 3 + + # sourcerate = 1 + destrate = 1 + if destcurr.upper() == "EUR": + for rate in rates: + if rate == "date": + converted.append(rates[rate]) + if rate == sourcecurr.upper(): + converted.append( + (1 / decimal.Decimal(rates[rate])) * decimal.Decimal(amount) + ) + converted.append(dec_prec) + return converted + else: + for rate in rates: + if rate == "date": + converted.append(rates[rate]) + if rate == destcurr.upper(): + # If source is EURO then straight convert and return + if sourcecurr.upper() == "EUR": + converted.append( + decimal.Decimal(rates[rate]) * decimal.Decimal(amount) + ) + converted.append(dec_prec) + return converted + else: + destrate = rates[rate] + if rate == sourcecurr.upper(): + sourcerate = rates[rate] + # Convert via the EURO + sourceEuro = (1 / decimal.Decimal(sourcerate)) * decimal.Decimal(amount) + converted.append(decimal.Decimal(sourceEuro) * decimal.Decimal(destrate)) + converted.append(dec_prec) + return converted if __name__ == "__main__": diff --git a/plugin/utils.py b/plugin/utils.py deleted file mode 100644 index 5db6a4d..0000000 --- a/plugin/utils.py +++ /dev/null @@ -1,154 +0,0 @@ -import requests -import decimal -import xml.etree.ElementTree as ET -import os -import datetime -import math - - -def populate_rates(xml): - """Extracts conversion rates from European Central Bank XML file - - Note :- This ONLY uses the daily file, not historical rates - - Parameters - ---------- - self: - Method instance - xml : path - XML file to process, assumed to be in plugin directory - - Returns - ------- - dict - Dictionary where first pair is the date from the XML file and - following pairs are currenct and rate (against the EURO) - """ - - tree = ET.parse(xml) - root = tree.getroot() - rates = {} - for root_Cube in root.findall( - "{http://www.ecb.int/vocabulary/2002-08-01/eurofxref}Cube" - ): - for time_Cube in root_Cube.findall( - "{http://www.ecb.int/vocabulary/2002-08-01/eurofxref}Cube" - ): - rates.update({"date": "{}".format(time_Cube.attrib["time"])}) - for currency_Cube in time_Cube.findall( - "{http://www.ecb.int/vocabulary/2002-08-01/eurofxref}Cube" - ): - rates.update( - { - "{}".format(currency_Cube.attrib["currency"]): "{}".format( - currency_Cube.attrib["rate"] - ) - } - ) - return rates - - -def getrates_xml(): - """Generate XML file of conversion rates from European Central Bank - - If an XML file already exists in the plugin directory, and it is less - than two hours old it will use that. Otherwise it gets a fresh copy from - the ECB web site - - Parameters - ---------- - self: - Method instance - - Returns - ------- - str - Status code of the web call. Forced to '200' if it just uses the - existing file - """ - - xmlfile = "eurofxref-daily.xml" - exists = os.path.isfile(xmlfile) - getnewfile = True - if exists: - current = datetime.datetime.now() - t = os.path.getmtime(xmlfile) - file = datetime.datetime.fromtimestamp(t) - if (current - file) > datetime.timedelta(hours=2): - getnewfile = True - else: - getnewfile = False - if getnewfile: - URL = "http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml" - r = requests.get(URL) - with open(xmlfile, "wb") as file: - file.write(r.content) - return r.status_code - else: - return 200 - - -def currconv(rates, sourcecurr, destcurr, amount): - """Converts from one currency to another - - Parameters - ---------- - self: - Method instance - rates: dict - Dictionary generated in populate_rates - sourcecurr: str - Source currency three char code - destcurr: str - Destination currency three char code - amount: str - Amount to convert - - Returns - ------- - list - Date from the currency XML file and the converted amount, and the precision - """ - - converted = [] - - # Change the decimal precision to match the number of digits in the amount - if "." in amount: - dec_prec = len(amount.split(".")[1]) - # Default to precision of 3 decimal places - else: - dec_prec = 3 - - # sourcerate = 1 - destrate = 1 - if destcurr.upper() == "EUR": - for rate in rates: - if rate == "date": - converted.append(rates[rate]) - if rate == sourcecurr.upper(): - converted.append( - (1 / decimal.Decimal(rates[rate])) * decimal.Decimal(amount) - ) - converted.append(dec_prec) - return converted - else: - for rate in rates: - if rate == "date": - converted.append(rates[rate]) - if rate == destcurr.upper(): - # If source is EURO then straight convert and return - if sourcecurr.upper() == "EUR": - converted.append( - decimal.Decimal(rates[rate]) * decimal.Decimal(amount) - ) - converted.append(dec_prec) - return converted - else: - destrate = rates[rate] - if rate == sourcecurr.upper(): - sourcerate = rates[rate] - # Convert via the EURO - sourceEuro = (1 / decimal.Decimal(sourcerate)) * decimal.Decimal(amount) - converted.append(decimal.Decimal(sourceEuro) * decimal.Decimal(destrate)) - converted.append(dec_prec) - return converted \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 6ee4979..7b1ce4c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,4 +3,5 @@ flox python-dotenv requests typing -babel \ No newline at end of file +babel +click \ No newline at end of file diff --git a/translations_helper.py b/translations_helper.py new file mode 100644 index 0000000..06d38df --- /dev/null +++ b/translations_helper.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +import json +import os + +import click + + +@click.group() +def translate(): + """Translation and localization commands.""" + ... + + +@translate.command() +@click.argument("locale") +def init(locale): + """Initialize a new language.""" + + if os.system("pybabel extract -F babel.cfg -k _l -o messages.pot ."): + raise RuntimeError("extract command failed") + if os.system("pybabel init -i messages.pot -d plugin/translations -l " + locale): + raise RuntimeError("init command failed") + os.remove("messages.pot") + + click.echo("Done.") + + +@translate.command() +def update(): + """Update all languages.""" + if os.system("pybabel extract -F babel.cfg -k _l -o messages.pot ."): + raise RuntimeError("extract command failed") + if os.system("pybabel update -i messages.pot -d plugin/translations"): + raise RuntimeError("update command failed") + os.remove("messages.pot") + + click.echo("Done.") + + +@translate.command() +def compile(): + """Compile all languages.""" + if os.system("pybabel compile -d plugin/translations"): + raise RuntimeError("compile command failed") + + click.echo("Done.") + + +cli = click.CommandCollection(sources=[translate]) + +if __name__ == "__main__": + cli() From f95a4c459be12867c3c94e628dfdfbbddcdc20c8 Mon Sep 17 00:00:00 2001 From: Damien Date: Thu, 31 Mar 2022 18:47:10 +1100 Subject: [PATCH 4/9] update translations --- plugin/currency_converter.py | 63 +++++++++--------- .../translations/en/LC_MESSAGES/messages.mo | Bin 907 -> 1285 bytes .../translations/en/LC_MESSAGES/messages.po | 54 ++++++++++----- .../translations/zh/LC_MESSAGES/messages.mo | Bin 872 -> 1181 bytes .../translations/zh/LC_MESSAGES/messages.po | 53 +++++++++++---- translations_helper.py | 3 - 6 files changed, 109 insertions(+), 64 deletions(-) diff --git a/plugin/currency_converter.py b/plugin/currency_converter.py index 8705abd..640b7e6 100644 --- a/plugin/currency_converter.py +++ b/plugin/currency_converter.py @@ -76,42 +76,41 @@ def query(self, query): self.add_item(title=_("Error - {} not a valid currency")).format( args[2].upper() ) + + # If source and dest currencies the same just return entered amount + elif args[1].upper() == args[2].upper(): + self.add_item( + title="{} {} = {} {}".format( + args[0], args[1].upper(), args[0], args[2].upper() + ) + ) # Do the conversion else: - # If source and dest currencies the same just return entered amount - if args[1].upper() == args[2].upper(): + # First strip any commas from the amount + args[0] = args[0].replace(",", "") + ratesxml_returncode = self.getrates_xml(self.max_age) + if ratesxml_returncode == 200: + ratedict = self.populate_rates("eurofxref-daily.xml") + conv = self.currconv(ratedict, args[1], args[2], args[0]) + # decimal.getcontext().prec = conv[2] self.add_item( - title="{} {} = {} {}".format( - args[0], args[1].upper(), args[0], args[2].upper() - ) + title=( + f"{locale.format_string('%.3f', float(args[0]), grouping=True)} {args[1].upper()} = " + f"{locale.format_string('%.3f', round(decimal.Decimal(conv[1]), conv[2]), grouping=True)} " + f"{args[2].upper()} " + f"(1 {args[1].upper()} = " + f"{round(decimal.Decimal(conv[1]) / decimal.Decimal(args[0]),conv[2],)} " + f"{args[2].upper()})" + ), + subtitle=_("Rates date : {}").format(conv[0]), ) else: - - # First strip any commas from the amount - args[0] = args[0].replace(",", "") - ratesxml_returncode = self.getrates_xml(self.max_age) - if ratesxml_returncode == 200: - ratedict = self.populate_rates("eurofxref-daily.xml") - conv = self.currconv(ratedict, args[1], args[2], args[0]) - # decimal.getcontext().prec = conv[2] - self.add_item( - title=( - f"{locale.format_string('%.3f', float(args[0]), grouping=True)} {args[1].upper()} = " - f"{locale.format_string('%.3f', round(decimal.Decimal(conv[1]), conv[2]), grouping=True)} " - f"{args[2].upper()} " - f"(1 {args[1].upper()} = " - f"{round(decimal.Decimal(conv[1]) / decimal.Decimal(args[0]),conv[2],)} " - f"{args[2].upper()})" - ), - subtitle=f"Rates date : {conv[0]}", - ) - else: - self.add_item( - title=_("Couldn't download the rates file"), - subtitle=_( - f"{ratesxml_returncode} - check log for more details" - ), - ) + self.add_item( + title=_("Couldn't download the rates file"), + subtitle=_("{} - check log for more details").format( + ratesxml_returncode + ), + ) # Always show the usage while there isn't a valid query else: @@ -130,7 +129,7 @@ def query(self, query): subtitle=(lines[0]), ) for line in range(1, len(lines)): - self.add_item(title="", subtitle=(lines[line]), icon="garbage") + self.add_item(title="", subtitle=(lines[line])) else: self.add_item( (title), diff --git a/plugin/translations/en/LC_MESSAGES/messages.mo b/plugin/translations/en/LC_MESSAGES/messages.mo index 1c8b83a98ca8c0f2b57b3f5c83bc93cb9e27ed0e..7731087c408016f9016f649b03e97ca5a912e060 100644 GIT binary patch literal 1285 zcmeH_!EO^V5QY~h2bKc}2_z(>4mVKcu)CG2cAHdflK_`@FRv{8 zZV|oi2GQ&85xs6r^tvZRult$kbx(;wd#~1way!s|svDCd@wEO?w{BUreOByx*@ z%Ao-_F-X!AZg}q3zg1>~L+;Q*L0?EoJIE$z5RQeEFhfTR_*qVN!>R1+K8N)&9rsVvKMN`Y7!ek^geTh%1d%5@!%o@A_R zaNc*;qjFP7q9kF_I!m@7Nzyo8i{mIFM|Oxu!sj{ni&&hnc%4GhWMkns;MTFsaAhTT z+N97GPP{?YqaL^aR>Dp=;HqkPMX6CO7c4_w`yFTx2D?(Uw5fK&k!%c??4dTbO+iiP zOWR4dVKMF8Q*gHf@%?akFzlZ1@ESpU!mBZKW!Bldgw&!nnPrLRBejj)e;!p+bML)Y@4J`+SU&`o7hQ!~d`RpTF{-slS3> delta 286 zcmZqW>SnLMC&V(90SMTESO$ngKpZ5{17uqPaW4=H0r3eS<^tl|Kr9c$&wyAPh}jq! z82Ev-5s;n?#7;mA(szvsqEC<+qE7`#gY@YGX`nuF27e#}q_G-EGuU|Kmt?0_+9^yt zD>?Zhv$(t+gL7$7QEFatrGj&QURi2UNoo;;Yf({tk%F#5b?sz*M#agFEH;znnPPYi bbPbJkjZ72_Ev-zAHg_;-F!C61k!>UZj#W1K diff --git a/plugin/translations/en/LC_MESSAGES/messages.po b/plugin/translations/en/LC_MESSAGES/messages.po index 39d6bc6..fa0e5ff 100644 --- a/plugin/translations/en/LC_MESSAGES/messages.po +++ b/plugin/translations/en/LC_MESSAGES/messages.po @@ -1,13 +1,13 @@ # English translations for Flow.plugin.currency. # Copyright (C) 2020 CitizenDee (author) # This file is distributed under the same license as the project. -# CitizenDee , 2020. +# CitizenDee , 2022. # msgid "" msgstr "" "Project-Id-Version: 2.0.0\n" "Report-Msgid-Bugs-To: deefrawley@gmail.com\n" -"POT-Creation-Date: 2020-12-24 19:52+1100\n" +"POT-Creation-Date: 2022-03-29 22:11+1100\n" "PO-Revision-Date: 2020-12-13 20:26+1100\n" "Last-Translator: CitizenDee \n" "Language: en\n" @@ -16,29 +16,53 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.9.0\n" +"Generated-By: Babel 2.9.1\n" -#: plugin/ui.py:80 +#: plugin/currency_converter.py:67 msgid "Please enter three character currency codes" msgstr "Please enter three character currency codes" -#: plugin/ui.py:86 plugin/ui.py:91 +#: plugin/currency_converter.py:71 plugin/currency_converter.py:76 msgid "Error - {} not a valid currency" msgstr "Error - {} not a valid currency" -#: plugin/ui.py:121 +#: plugin/currency_converter.py:106 msgid "Rates date : {}" msgstr "Rates date : {}" -#: plugin/ui.py:125 -msgid "Error - {}" -msgstr "Error - {}" +#: plugin/currency_converter.py:110 +msgid "Couldn't download the rates file" +msgstr "" + +#: plugin/currency_converter.py:111 +msgid "{} - check log for more details" +msgstr "{} - check log for more details" + +#: plugin/currency_converter.py:119 +msgid " " +msgstr " " + +#: plugin/currency_converter.py:120 +msgid "" +"There will be a short delay if the currency rates file needs to be " +"downloaded" +msgstr "" +"There will be a short delay if the currency rates file needs to be " +"downloaded" + +#: plugin/currency_converter.py:124 +msgid "Available currencies:" +msgstr "Available currencies:" + +#: plugin/currency_converter.py:186 +msgid "HTTP Error" +msgstr "HTTP Error" -#: plugin/ui.py:129 -msgid "Currency Converter" -msgstr "Currency Converter" +#: plugin/currency_converter.py:189 +msgid "Connection Error" +msgstr "Connection Error" -#: plugin/ui.py:130 -msgid " " -msgstr " " +#: plugin/currency_converter.py:192 +msgid "Unspecifed Download Error" +msgstr "Unspecifed Download Error" diff --git a/plugin/translations/zh/LC_MESSAGES/messages.mo b/plugin/translations/zh/LC_MESSAGES/messages.mo index 6a1be26bc7aec54f7c27acd40418172ba78ac004..844023cf94f35fd5dd5348510a3e26e8463c38aa 100644 GIT binary patch delta 658 zcmZY4Uu)A)7zXfDTg7$$$BBaI+bb`$U^^x_5F9FaBY08q%8M*HwZWK-By$WMwL>hj zQW@P=?CPpu>rk}F#vF8`cq{HJh+uM(f#8iFz$a~mfq{o#a`L|CJ;&ekvM>AnNMxSj z=|@f?cJQ&|`G)raWD7Zj{6q-(gPcYNLX1V>5IhSn!-G(RH{c_90)B--y#otKWEW$7 zjAhwun6Z8Mu?UaDWq1mHfx)4Ez!>}m55ofy#x6jDLH%ub4vs-K@<38GX;kARkDDgf zWtCgQ?24i5T((t1r)#EZm{=Is6#a}%$}wX?*9=J^JHyG8Y;I9n)%dI%i(XHqZg%TS zyF-__A$T8W0(PlCWQw*T@zkxS^jiKBar)i_PS>a j+M!$q2>rBm;, 2020. +# CitizenDee , 2022. # msgid "" msgstr "" "Project-Id-Version: 2.0.0\n" "Report-Msgid-Bugs-To: deefrawley@gmail.com\n" -"POT-Creation-Date: 2020-12-24 19:52+1100\n" +"POT-Creation-Date: 2022-03-29 22:11+1100\n" "PO-Revision-Date: 2020-12-13 20:56+1100\n" "Last-Translator: CitizenDee \n" "Language: zh\n" @@ -16,29 +16,54 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.9.0\n" +"Generated-By: Babel 2.9.1\n" -#: plugin/ui.py:80 +#: plugin/currency_converter.py:67 msgid "Please enter three character currency codes" msgstr "输入三个字符的货币代码" -#: plugin/ui.py:86 plugin/ui.py:91 +#: plugin/currency_converter.py:71 plugin/currency_converter.py:76 msgid "Error - {} not a valid currency" msgstr "错误 - {} 不是有效货币" -#: plugin/ui.py:121 +#: plugin/currency_converter.py:106 msgid "Rates date : {}" msgstr "汇率日期 : {}" -#: plugin/ui.py:125 -msgid "Error - {}" -msgstr "错误 - {}" +#: plugin/currency_converter.py:110 +msgid "Couldn't download the rates file" +msgstr "无法下载汇率文件" -#: plugin/ui.py:129 -msgid "Currency Converter" -msgstr "货币兑换" +#: plugin/currency_converter.py:111 +msgid "{} - check log for more details" +msgstr "{} - 查看日志以获取更多详细信息" -#: plugin/ui.py:130 -msgid " " +#: plugin/currency_converter.py:119 +#, fuzzy +msgid " " msgstr "<热键> <量> <来源货币> <目标货币>" +#: plugin/currency_converter.py:120 +msgid "" +"There will be a short delay if the currency rates file needs to be " +"downloaded" +msgstr "" +"如果需要下载汇率文件,会有短暂的延迟" + +#: plugin/currency_converter.py:124 +msgid "Available currencies:" +msgstr 可用货币" + +#: plugin/currency_converter.py:186 +msgid "HTTP Error" +msgstr "HTTP 错误" + +#: plugin/currency_converter.py:189 +msgid "Connection Error" +msgstr "连接错误" + +#: plugin/currency_converter.py:192 +msgid "Unspecifed Download Error" +msgstr "未指定的下载错误" + + diff --git a/translations_helper.py b/translations_helper.py index 06d38df..c014e55 100644 --- a/translations_helper.py +++ b/translations_helper.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- - -import json import os - import click From 72d092636738934f287a31940637150a4056a515 Mon Sep 17 00:00:00 2001 From: Damien Date: Fri, 22 Apr 2022 17:52:01 +1000 Subject: [PATCH 5/9] Updated help function and check for valid amount --- plugin/currency_converter.py | 160 ++++++++++-------- .../translations/en/LC_MESSAGES/messages.mo | Bin 1285 -> 1461 bytes .../translations/en/LC_MESSAGES/messages.po | 73 +++++--- .../translations/zh/LC_MESSAGES/messages.mo | Bin 1181 -> 1282 bytes .../translations/zh/LC_MESSAGES/messages.po | 69 +++++--- 5 files changed, 174 insertions(+), 128 deletions(-) diff --git a/plugin/currency_converter.py b/plugin/currency_converter.py index 640b7e6..a755b2f 100644 --- a/plugin/currency_converter.py +++ b/plugin/currency_converter.py @@ -61,80 +61,96 @@ def __init__(self, *args, **kwargs): def query(self, query): q = query.strip() args = q.split(" ") - if len(args) == 3: - # Check codes are three letters - if len(args[1]) != 3 or len(args[2]) != 3: - self.add_item(title=_("Please enter three character currency codes")) - - # Check first argument is valid currency code - elif len(args[1]) == 3 and args[1].upper() not in self.CURRENCIES: - self.add_item(title=_("Error - {} not a valid currency")).format( - args[1].upper() - ) - # Check second argument is valid currency code - elif len(args[2]) == 3 and args[2].upper() not in self.CURRENCIES: - self.add_item(title=_("Error - {} not a valid currency")).format( - args[2].upper() - ) + if len(args) == 1: + self.add_item( + title=_(" "), + subtitle=_( + "There will be a short delay if the currency rates file needs to be downloaded" + ), + ) - # If source and dest currencies the same just return entered amount - elif args[1].upper() == args[2].upper(): + elif len(args) == 2: + hint = self.applicablerates(args[1]) + self.add_item( + title=(f", ".join([f"{x}" for x in hint])), + subtitle=_("Source currency"), + ) + elif len(args) == 3: + if len(args[2]) <= 2: + hint = self.applicablerates(args[2]) self.add_item( - title="{} {} = {} {}".format( - args[0], args[1].upper(), args[0], args[2].upper() - ) + title=(f", ".join([f"{x}" for x in hint])), + subtitle=_("Destination currency"), ) - # Do the conversion else: - # First strip any commas from the amount - args[0] = args[0].replace(",", "") - ratesxml_returncode = self.getrates_xml(self.max_age) - if ratesxml_returncode == 200: - ratedict = self.populate_rates("eurofxref-daily.xml") - conv = self.currconv(ratedict, args[1], args[2], args[0]) - # decimal.getcontext().prec = conv[2] + # Check codes are three letters + if len(args[1]) != 3 or len(args[2]) != 3: self.add_item( - title=( - f"{locale.format_string('%.3f', float(args[0]), grouping=True)} {args[1].upper()} = " - f"{locale.format_string('%.3f', round(decimal.Decimal(conv[1]), conv[2]), grouping=True)} " - f"{args[2].upper()} " - f"(1 {args[1].upper()} = " - f"{round(decimal.Decimal(conv[1]) / decimal.Decimal(args[0]),conv[2],)} " - f"{args[2].upper()})" - ), - subtitle=_("Rates date : {}").format(conv[0]), + title=_("Please enter three character currency codes") ) - else: + + # Check first argument is valid currency code + elif len(args[1]) == 3 and args[1].upper() not in self.CURRENCIES: + self.add_item(title=_("Error - source is not a valid currency")) + + # Check second argument is valid currency code + elif len(args[2]) == 3 and args[2].upper() not in self.CURRENCIES: self.add_item( - title=_("Couldn't download the rates file"), - subtitle=_("{} - check log for more details").format( - ratesxml_returncode - ), + title=_("Error - destination is not a valid currency") + ) + elif not args[0].isdigit(): + self.add_item(title=_("Error - amount must be numeric")) + + # If source and dest currencies the same just return entered amount + elif args[1].upper() == args[2].upper(): + self.add_item( + title="{} {} = {} {}".format( + args[0], args[1].upper(), args[0], args[2].upper() + ) ) + # Do the conversion + else: + # First strip any commas from the amount + args[0] = args[0].replace(",", "") + # Get the rates + ratesxml_returncode = self.getrates_xml(self.max_age) + if ratesxml_returncode == 200: + ratedict = self.populate_rates("eurofxref-daily.xml") + conv = self.currconv(ratedict, args[1], args[2], args[0]) + # Set up some decimal precisions to use in the result + # amount and converted amount use precision as entered. Conversation rate uses min 3 places + if "." in args[0]: + dec_prec = len(args[0].split(".")[1]) + if dec_prec < 3: + dec_prec2 = 3 + else: + dec_prec2 = dec_prec + else: + dec_prec = 0 + dec_prec2 = 3 + fmt_str = "%.{0:d}f".format(dec_prec) + + self.add_item( + title=( + f"{locale.format_string(fmt_str, float(args[0]), grouping=True)} {args[1].upper()} = " + f"{locale.format_string(fmt_str, round(decimal.Decimal(conv[1]), dec_prec), grouping=True)} " + f"{args[2].upper()} " + f"(1 {args[1].upper()} = " + f"{round(decimal.Decimal(conv[1]) / decimal.Decimal(args[0]),dec_prec2,)} " + f"{args[2].upper()})" + ), + subtitle=_("Rates date : {}").format(conv[0]), + ) + else: + self.add_item( + title=_("Couldn't download the rates file"), + subtitle=_("{} - check log for more details").format( + ratesxml_returncode + ), + ) - # Always show the usage while there isn't a valid query else: - self.add_item( - title=_(" "), - subtitle=_( - "There will be a short delay if the currency rates file needs to be downloaded" - ), - ) - title = _("Available currencies:") - subtitle = ", ".join(self.CURRENCIES) - lines = textwrap.wrap(subtitle, 110) - if len(lines) > 1: - self.add_item( - title=(title), - subtitle=(lines[0]), - ) - for line in range(1, len(lines)): - self.add_item(title="", subtitle=(lines[line])) - else: - self.add_item( - (title), - (subtitle), - ) + pass def populate_rates(self, xml): tree = ET.parse(xml) @@ -195,13 +211,6 @@ def getrates_xml(self, max_age): def currconv(self, rates, sourcecurr, destcurr, amount): converted = [] - # Change the decimal precision to match the number of digits in the amount - if "." in amount: - dec_prec = len(amount.split(".")[1]) - # Default to precision of 3 decimal places - else: - dec_prec = 3 - # sourcerate = 1 destrate = 1 if destcurr.upper() == "EUR": @@ -212,7 +221,6 @@ def currconv(self, rates, sourcecurr, destcurr, amount): converted.append( (1 / decimal.Decimal(rates[rate])) * decimal.Decimal(amount) ) - converted.append(dec_prec) return converted else: for rate in rates: @@ -224,7 +232,6 @@ def currconv(self, rates, sourcecurr, destcurr, amount): converted.append( decimal.Decimal(rates[rate]) * decimal.Decimal(amount) ) - converted.append(dec_prec) return converted else: destrate = rates[rate] @@ -233,9 +240,14 @@ def currconv(self, rates, sourcecurr, destcurr, amount): # Convert via the EURO sourceEuro = (1 / decimal.Decimal(sourcerate)) * decimal.Decimal(amount) converted.append(decimal.Decimal(sourceEuro) * decimal.Decimal(destrate)) - converted.append(dec_prec) return converted + def applicablerates(self, ratestr): + choices = [i for i in self.CURRENCIES if i.upper().startswith(ratestr.upper())] + if not choices: + choices.append(_("No matches found")) + return choices + if __name__ == "__main__": Currency() diff --git a/plugin/translations/en/LC_MESSAGES/messages.mo b/plugin/translations/en/LC_MESSAGES/messages.mo index 7731087c408016f9016f649b03e97ca5a912e060..110ee3491f452a56c5a7e36547e120c36bf99c4a 100644 GIT binary patch delta 596 zcmd7Mu}T9$5C-7MaUw)wL~H~#0~Qt`U_wAN3PKb_Py}Ng&byTy=5}SXhloOKEJQ)V zk=o9}R;;XT#U~J~d;}W>|4kzJ2rdlY&hG8}caeS^X}^uTUBVhgO`)!B^JjfwUqOAN z#!!nXB8HoA0v^H{cn;I>7TWLbVIDrgEc_Vw3AfM>J4BO2ZQ8>ohXVx@T;4z%IP3NY z)ghxlf;O-NZQvcWfzQwezCs)L4sBp&sP8nifdz;_I)yf`3u(FYQfXI}l)^WmlDw|9 z(o|J(GmvwJ19dE$$_v;u1Z&TT79WI7L2IIALh1FM__5YP`X|(T%LVo#6-&cW+%mi` zSjLgip-&sN+RkA9w&KV$e#4Gau_J?I)m_Q%s#yDif9EAX~w(RN~^oQn{2b N&Ocl^LFym>@eebyjg$ZY delta 414 zcmdnW-O5#ePl#nI0}yZnu?!HGfH+9t43KRN#Akt66o@$)85jhBv^P>i+}j$sjfp*j#Uh&CC#k-veoo!K^G0gg}CzeFKXp)4^cGX\n" "Language: en\n" @@ -18,51 +18,70 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.1\n" +#: plugin/currency_converter.py:66 +msgid " " +msgstr " " + #: plugin/currency_converter.py:67 +msgid "" +"There will be a short delay if the currency rates file needs to be " +"downloaded" +msgstr "" +"There will be a short delay if the currency rates file needs to be " +"downloaded" + +#: plugin/currency_converter.py:76 +msgid "Source currency" +msgstr "Source currency" + +#: plugin/currency_converter.py:83 +msgid "Destination currency" +msgstr "Destination currency" + +#: plugin/currency_converter.py:89 msgid "Please enter three character currency codes" msgstr "Please enter three character currency codes" -#: plugin/currency_converter.py:71 plugin/currency_converter.py:76 -msgid "Error - {} not a valid currency" -msgstr "Error - {} not a valid currency" +#: plugin/currency_converter.py:94 +#, fuzzy +msgid "Error - source is not a valid currency" +msgstr "Error - source is not a valid currency" + +#: plugin/currency_converter.py:99 +#, fuzzy +msgid "Error - destination is not a valid currency" +msgstr "Error - destination not a valid currency" + +#: plugin/currency_converter.py:102 +msgid "Error - amount must be numeric" +msgstr "Error - amount must be numeric" -#: plugin/currency_converter.py:106 +#: plugin/currency_converter.py:142 msgid "Rates date : {}" msgstr "Rates date : {}" -#: plugin/currency_converter.py:110 +#: plugin/currency_converter.py:146 msgid "Couldn't download the rates file" -msgstr "" +msgstr "Couldn't download the rates file" -#: plugin/currency_converter.py:111 +#: plugin/currency_converter.py:147 msgid "{} - check log for more details" msgstr "{} - check log for more details" -#: plugin/currency_converter.py:119 -msgid " " -msgstr " " - -#: plugin/currency_converter.py:120 -msgid "" -"There will be a short delay if the currency rates file needs to be " -"downloaded" -msgstr "" -"There will be a short delay if the currency rates file needs to be " -"downloaded" - -#: plugin/currency_converter.py:124 -msgid "Available currencies:" -msgstr "Available currencies:" - -#: plugin/currency_converter.py:186 +#: plugin/currency_converter.py:201 msgid "HTTP Error" msgstr "HTTP Error" -#: plugin/currency_converter.py:189 +#: plugin/currency_converter.py:204 msgid "Connection Error" msgstr "Connection Error" -#: plugin/currency_converter.py:192 +#: plugin/currency_converter.py:207 msgid "Unspecifed Download Error" msgstr "Unspecifed Download Error" +#: plugin/currency_converter.py:248 +msgid "No matches found" +msgstr "No matches found" + + diff --git a/plugin/translations/zh/LC_MESSAGES/messages.mo b/plugin/translations/zh/LC_MESSAGES/messages.mo index 844023cf94f35fd5dd5348510a3e26e8463c38aa..be141731076411d294ce951e3e6f9160e5888c36 100644 GIT binary patch delta 489 zcmYk#&npCB7zglYb~m-G7CBI|?R#_4f(P5 zKuHn1#lc$fFSs~zo3Rsaa+L4vQr`OZd4D|b^Q7G?iE|efYQDv9Cft z5QcmswTO=+YJ_1}4#%KfHvt1shF+NZ^&Af2oQL($?LZHrC2GPZiOW9d!Na4Is180u zyTKQ<2bH;qDqu6LfkEhp{jeHNL;L?6?1Twu_ql_u@Cj0%EJ*^dMrDZ&D2lAm)~A1@ zUx=wuDN?-UHAN95ex8cYtc^uPS(8*IYB7~f3QW>OL5cD-I6gl5+hSN|VnpR2P)y2GJBu>ES*;pg;}vt{Ttb@_S_Hx9Ff>r>-q znXK*oLMDUJ*6NmZoHia3)=}Qv%a}V^<6^foxl}{u;hDL9Z>(jFWX?)#SVuYi&Dr5G P(uf(0b+R|7G-2$-~!U7 zKw29}I{;~ZAe{rGeSvfhkd_3}r+_p_{SzSF1H>#$3=EtM49N@=nHd;(fD*HTv;>e| z0i@-C^g$pEa?}GLtq7#w0BJ!W&BwyPU(gi@8!LckcGbb@ACsiT2 zw5TXGFF7-{*lOY%V=dRBqWmHSU4`meg}nR{g+zt2#GK3&n5s$!kC2c61+b#Yd5pG` zcQZccHP$t\n" "Language: zh\n" @@ -18,52 +18,67 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.1\n" +#: plugin/currency_converter.py:66 +#, fuzzy +msgid " " +msgstr "<热键> <量> <来源货币> <目标货币>" + #: plugin/currency_converter.py:67 +msgid "" +"There will be a short delay if the currency rates file needs to be " +"downloaded" +msgstr "如果需要下载汇率文件,会有短暂的延迟" + +#: plugin/currency_converter.py:76 +msgid "Source currency" +msgstr "来源货币" + +#: plugin/currency_converter.py:83 +msgid "Destination currency" +msgstr "目的地货币" + +#: plugin/currency_converter.py:89 msgid "Please enter three character currency codes" msgstr "输入三个字符的货币代码" -#: plugin/currency_converter.py:71 plugin/currency_converter.py:76 -msgid "Error - {} not a valid currency" -msgstr "错误 - {} 不是有效货币" +#: plugin/currency_converter.py:94 +#, fuzzy +msgid "Error - source is not a valid currency" +msgstr "错误 - 来源货币不是有效货币" + +#: plugin/currency_converter.py:99 +#, fuzzy +msgid "Error - destination is not a valid currency" +msgstr "错误 - 目的地货币不是有效货币" -#: plugin/currency_converter.py:106 +#: plugin/currency_converter.py:102 +msgid "Error - amount must be numeric" +msgstr "错误 - 金额必须是数字" + +#: plugin/currency_converter.py:142 msgid "Rates date : {}" msgstr "汇率日期 : {}" -#: plugin/currency_converter.py:110 +#: plugin/currency_converter.py:146 msgid "Couldn't download the rates file" msgstr "无法下载汇率文件" -#: plugin/currency_converter.py:111 +#: plugin/currency_converter.py:147 msgid "{} - check log for more details" msgstr "{} - 查看日志以获取更多详细信息" -#: plugin/currency_converter.py:119 -#, fuzzy -msgid " " -msgstr "<热键> <量> <来源货币> <目标货币>" - -#: plugin/currency_converter.py:120 -msgid "" -"There will be a short delay if the currency rates file needs to be " -"downloaded" -msgstr "" -"如果需要下载汇率文件,会有短暂的延迟" - -#: plugin/currency_converter.py:124 -msgid "Available currencies:" -msgstr 可用货币" - -#: plugin/currency_converter.py:186 +#: plugin/currency_converter.py:201 msgid "HTTP Error" msgstr "HTTP 错误" -#: plugin/currency_converter.py:189 +#: plugin/currency_converter.py:204 msgid "Connection Error" msgstr "连接错误" -#: plugin/currency_converter.py:192 +#: plugin/currency_converter.py:207 msgid "Unspecifed Download Error" msgstr "未指定的下载错误" - +#: plugin/currency_converter.py:248 +msgid "No matches found" +msgstr "未找到匹配项" From 4fca5aa33a6b4cc0c82ed0239f24bd9ccd10cde4 Mon Sep 17 00:00:00 2001 From: Damien Date: Fri, 22 Apr 2022 17:58:27 +1000 Subject: [PATCH 6/9] update requirements to correct flox package --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6e3e73f..4fc49d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ flowlauncher -flox +flox-lib python-dotenv requests typing \ No newline at end of file From de608cfebaead277638c891c137e7618d46783c1 Mon Sep 17 00:00:00 2001 From: Damien Date: Fri, 22 Apr 2022 18:19:45 +1000 Subject: [PATCH 7/9] Fix amount int/float test --- plugin/currency_converter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugin/currency_converter.py b/plugin/currency_converter.py index a755b2f..b23c511 100644 --- a/plugin/currency_converter.py +++ b/plugin/currency_converter.py @@ -98,7 +98,11 @@ def query(self, query): self.add_item( title=_("Error - destination is not a valid currency") ) - elif not args[0].isdigit(): + + # Check amount is an int or a float + elif ( + not args[0].isdigit() and not args[0].replace(".", "", 1).isdigit() + ): self.add_item(title=_("Error - amount must be numeric")) # If source and dest currencies the same just return entered amount From 66b0f4cc441dfab9e0eaa2442eb78dc9ddbd4092 Mon Sep 17 00:00:00 2001 From: Damien Date: Fri, 22 Apr 2022 18:29:28 +1000 Subject: [PATCH 8/9] Update README and screenshot --- README.md | 33 ++++++++++++++------------------- assets/cc_screenshot.png | Bin 27255 -> 12821 bytes plugin/currency_converter.py | 1 + 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 6b8025f..673e88a 100644 --- a/README.md +++ b/README.md @@ -10,37 +10,28 @@ Uses the [European Central Bank](https://www.ecb.europa.eu/stats/policy_and_exch Currency code that can be used are: -'AUD' , 'BGN' , 'BRL' , 'CAD' , 'CHF' , 'CNY' , 'CZK' , 'DKK' , 'GBP', 'HKD' , 'HRK' , 'HUF' , 'IDR' , 'ILS' , 'INR' , 'ISK' , 'JPY' , 'KRW', 'MXN','MYR' , 'NOK' , 'NZD' , 'PHP' , 'PLN' , 'RON' , 'RUB' , 'SEK', 'SGD' , 'THB' , 'TRY' , 'USD' , 'ZAR' , 'EUR' +'AUD' , 'BGN' , 'BRL' , 'CAD' , 'CHF' , 'CNY' , 'CZK' , 'DKK' , 'GBP', 'HKD' , 'HRK' , 'HUF' , 'IDR' , 'ILS' , 'INR' , 'ISK' , 'JPY' , 'KRW', 'MXN', 'MYR' , 'NOK' , 'NZD' , 'PHP' , 'PLN' , 'RON' , 'RUB' , 'SEK', 'SGD' , 'THB' , 'TRY' , 'USD' , 'ZAR' , 'EUR' ### Requirements -Python 3.5 or later installed on your system, with python.exe in your PATH variable and this path updated in the Flow Launcher settings (this is a general requirement to use Python plugins with Flow). As of v1.8, Flow Launcher should take care of the installation of Python for you if it is not on your system. +Flow Launcher should take care of the installation of Python for you if it is not on your system, as well as the libraries needed by this plugin. -You must be online when you run the plugin in Flow to download the currency XML file, or you will get a connection error. If the local XML file is 2 hours or less old (that is, you've run the plugin before in the previous two hours), it will just use the local file. +You must be online when you run the plugin in Flow to download the currency XML file, or you will get a connection error. A local copy of the local XML file froma previous download can be used if it exists. -### Installing - -#### Package Manager - -Use the `pm install` command from within Flow itself. - -#### Manual +### Plugin Settings -Add the Flow.Launcher.Plugin.Currency directory to %APPDATA%\Roaming\FlowLauncher\Plugins\ and restart Flow. +Type `settings` in Flow to access the settings window, then go to Plugins - Currency Converter -#### Python Package Requirements +__Keyword (default 'cc')__ - change this to set a new keyword to activate the plugin -There is no requirement to install the packages as they will be packed with the release. +__File age (default 6)__ - the age in hours for the local copy of the rates file to be used before the plugin downloads a fresh copy -If you still want to manually pip install them: -The `requirements.txt` file in this repo outlines which packages are needed. This can be found online here on Github, as well as in the local plugin directory once installed (%APPDATA%\Roaming\FlowLauncher\Plugins\Currency Converter-X.X.X\ where X.X.X is the currently installed version) - -The easiest way to manually install these packages is to use the following command in a Windows Command Prompt or Powershell Prompt +### Installing -`pip install -r requirements.txt -t ./lib` +#### Package Manager -Remember you need to be in the local directory containing the requirements text file. +Use the `pm install` command from within Flow itself. ### Localisation @@ -52,6 +43,10 @@ Currently English and Chinese language supported. Edit the .env file to change t | ---------------------------------------------------------------- | ------------------------------------------- | | `cc {amount} {source currency code} {destination currency code}` | Convert amount from source to dest currency | +When typing the source or destination currency the plugin will show what currencies are available. + +Decimal precision is based on the input amount however the base rate is always a minimum of three decimal places. + ### Problems, errors and feature requests Open an issue in this repo. diff --git a/assets/cc_screenshot.png b/assets/cc_screenshot.png index ff6ad41b48131e0ad30c6a1982064a4d863c3463..1567d7ef8737ebf0f9648d5e7390f6139c9fe251 100644 GIT binary patch literal 12821 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 literal 27255 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�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 Date: Fri, 22 Apr 2022 19:01:30 +1000 Subject: [PATCH 9/9] Handle error where expected rate is missing from XML (and update error message) --- plugin/currency_converter.py | 61 +++++++++++------- .../translations/en/LC_MESSAGES/messages.mo | Bin 1461 -> 1609 bytes .../translations/en/LC_MESSAGES/messages.po | 37 ++++++----- .../translations/zh/LC_MESSAGES/messages.mo | Bin 1282 -> 1425 bytes .../translations/zh/LC_MESSAGES/messages.po | 37 ++++++----- 5 files changed, 78 insertions(+), 57 deletions(-) diff --git a/plugin/currency_converter.py b/plugin/currency_converter.py index 30096b7..77b477a 100644 --- a/plugin/currency_converter.py +++ b/plugin/currency_converter.py @@ -16,6 +16,7 @@ class Currency(Flox): locale.setlocale(locale.LC_ALL, "") + ratesURL = "http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml" # TODO - save list to settings and update from each XML download and just use this list as a first time default CURRENCIES = [ "AUD", @@ -122,30 +123,34 @@ def query(self, query): if ratesxml_returncode == 200: ratedict = self.populate_rates("eurofxref-daily.xml") conv = self.currconv(ratedict, args[1], args[2], args[0]) - # Set up some decimal precisions to use in the result - # amount and converted amount use precision as entered. Conversation rate uses min 3 places - if "." in args[0]: - dec_prec = len(args[0].split(".")[1]) - if dec_prec < 3: - dec_prec2 = 3 - else: - dec_prec2 = dec_prec + if len(conv) == 1: + # Something has gone wrong + self.add_item(title="{}".format(conv[0])) else: - dec_prec = 0 - dec_prec2 = 3 - fmt_str = "%.{0:d}f".format(dec_prec) + # Set up some decimal precisions to use in the result + # amount and converted amount use precision as entered. Conversation rate uses min 3 places + if "." in args[0]: + dec_prec = len(args[0].split(".")[1]) + if dec_prec < 3: + dec_prec2 = 3 + else: + dec_prec2 = dec_prec + else: + dec_prec = 0 + dec_prec2 = 3 + fmt_str = "%.{0:d}f".format(dec_prec) - self.add_item( - title=( - f"{locale.format_string(fmt_str, float(args[0]), grouping=True)} {args[1].upper()} = " - f"{locale.format_string(fmt_str, round(decimal.Decimal(conv[1]), dec_prec), grouping=True)} " - f"{args[2].upper()} " - f"(1 {args[1].upper()} = " - f"{round(decimal.Decimal(conv[1]) / decimal.Decimal(args[0]),dec_prec2,)} " - f"{args[2].upper()})" - ), - subtitle=_("Rates date : {}").format(conv[0]), - ) + self.add_item( + title=( + f"{locale.format_string(fmt_str, float(args[0]), grouping=True)} {args[1].upper()} = " + f"{locale.format_string(fmt_str, round(decimal.Decimal(conv[1]), dec_prec), grouping=True)} " + f"{args[2].upper()} " + f"(1 {args[1].upper()} = " + f"{round(decimal.Decimal(conv[1]) / decimal.Decimal(args[0]),dec_prec2,)} " + f"{args[2].upper()})" + ), + subtitle=_("Rates date : {}").format(conv[0]), + ) else: self.add_item( title=_("Couldn't download the rates file"), @@ -195,8 +200,7 @@ def getrates_xml(self, max_age): getnewfile = False if getnewfile: try: - URL = "http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml" - r = requests.get(URL) + r = requests.get(self.ratesURL) with open(xmlfile, "wb") as file: file.write(r.content) self.logger.info(f"Download rates file returned {r.status_code}") @@ -215,6 +219,15 @@ def getrates_xml(self, max_age): def currconv(self, rates, sourcecurr, destcurr, amount): converted = [] + # Check source currency is in the rates dict -catch odd errors like the bank suspending some rates + if not sourcecurr.upper() in rates or not destcurr.upper() in rates: + self.logger.error( + f"Source or destination currency not in rates dict - {sourcecurr} or {destcurr}" + ) + converted.append( + _("Error - expected source or destination currency not in rates file") + ) + return converted # sourcerate = 1 destrate = 1 diff --git a/plugin/translations/en/LC_MESSAGES/messages.mo b/plugin/translations/en/LC_MESSAGES/messages.mo index 110ee3491f452a56c5a7e36547e120c36bf99c4a..8f4d3649d8b6c27d12a9802e7a4760fc997b8240 100644 GIT binary patch delta 427 zcmbu)u}T9m9LMooIXl=ZiU?8={jVa9g4O8;hYqen-O62r1IbA)2Z}>qL8-0<2XT~A ze1N(+_zHsb3Az<=@Efj!yMge@Klvv=^4fTh`k!mzgJP^OH<|ao_%otFsUzwpu3`rT z&u|@Ya0_oS#ur@18T$So+{I?C%4jd_;E7Uw)n(D5;TgAaf&(snp${&@>Y)(|^&9%& zDf-|!#<;Om1$WU0r|5%A^q+fNI6{Ke1Y0WNuwDi4)bGBZ;$v%bD|@1^E_LE`Dn(w} zL<^g#E?j1MF3XK1rM225*J5%fnGxG_x{&kiLXU^RX%HS9?zhMD+DGvJ2M5Do_6y8# BQiuQm delta 283 zcmYk#tqQ_m6oBFL=|u2nidY2yz$g|G#Gfk|7?-^OgLZ>nfFLrK#bU4yli6SpyaK^w z)nrldJc!TZk_4%p)nk%GufTrL$JjM3xe5;a(}!XFK>%0592u26$p)Zh^{ctH)`P=o1M iXbCmgK!P$w?fEEu&o-OjX1n#UKw<{$h3 diff --git a/plugin/translations/en/LC_MESSAGES/messages.po b/plugin/translations/en/LC_MESSAGES/messages.po index 518fd3f..6a897e7 100644 --- a/plugin/translations/en/LC_MESSAGES/messages.po +++ b/plugin/translations/en/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: 2.0.0\n" "Report-Msgid-Bugs-To: deefrawley@gmail.com\n" -"POT-Creation-Date: 2022-04-22 17:42+1000\n" +"POT-Creation-Date: 2022-04-22 18:59+1000\n" "PO-Revision-Date: 2020-12-13 20:26+1100\n" "Last-Translator: CitizenDee \n" "Language: en\n" @@ -18,11 +18,11 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.1\n" -#: plugin/currency_converter.py:66 +#: plugin/currency_converter.py:68 msgid " " msgstr " " -#: plugin/currency_converter.py:67 +#: plugin/currency_converter.py:69 msgid "" "There will be a short delay if the currency rates file needs to be " "downloaded" @@ -30,58 +30,61 @@ msgstr "" "There will be a short delay if the currency rates file needs to be " "downloaded" -#: plugin/currency_converter.py:76 +#: plugin/currency_converter.py:78 msgid "Source currency" msgstr "Source currency" -#: plugin/currency_converter.py:83 +#: plugin/currency_converter.py:85 msgid "Destination currency" msgstr "Destination currency" -#: plugin/currency_converter.py:89 +#: plugin/currency_converter.py:91 msgid "Please enter three character currency codes" msgstr "Please enter three character currency codes" -#: plugin/currency_converter.py:94 +#: plugin/currency_converter.py:96 #, fuzzy msgid "Error - source is not a valid currency" msgstr "Error - source is not a valid currency" -#: plugin/currency_converter.py:99 +#: plugin/currency_converter.py:101 #, fuzzy msgid "Error - destination is not a valid currency" msgstr "Error - destination not a valid currency" -#: plugin/currency_converter.py:102 +#: plugin/currency_converter.py:108 msgid "Error - amount must be numeric" msgstr "Error - amount must be numeric" -#: plugin/currency_converter.py:142 +#: plugin/currency_converter.py:152 msgid "Rates date : {}" msgstr "Rates date : {}" -#: plugin/currency_converter.py:146 +#: plugin/currency_converter.py:156 msgid "Couldn't download the rates file" msgstr "Couldn't download the rates file" -#: plugin/currency_converter.py:147 +#: plugin/currency_converter.py:157 msgid "{} - check log for more details" msgstr "{} - check log for more details" -#: plugin/currency_converter.py:201 +#: plugin/currency_converter.py:210 msgid "HTTP Error" msgstr "HTTP Error" -#: plugin/currency_converter.py:204 +#: plugin/currency_converter.py:213 msgid "Connection Error" msgstr "Connection Error" -#: plugin/currency_converter.py:207 +#: plugin/currency_converter.py:216 msgid "Unspecifed Download Error" msgstr "Unspecifed Download Error" -#: plugin/currency_converter.py:248 +#: plugin/currency_converter.py:228 +msgid "Error - expected source or destination currency not in rates file" +msgstr "Error - expected source or destination currency not in rates file" + +#: plugin/currency_converter.py:266 msgid "No matches found" msgstr "No matches found" - diff --git a/plugin/translations/zh/LC_MESSAGES/messages.mo b/plugin/translations/zh/LC_MESSAGES/messages.mo index be141731076411d294ce951e3e6f9160e5888c36..ce081e1dbdead0f0a02b895d03c7892e5c961a53 100644 GIT binary patch delta 409 zcmYk#&r1S96bJCPvJq%miHCxyDY{j7Dk{841wnM_5J4wdCs>Hx7+1=Z7{V4vB&ARp z{Q&7wSt%iPslTDSCmZ!pw{8i27dkZX<@4T~;mwXa<4II~YL;-kNI#Nxnt$g9bvM#N z7~)lk{4f9q;2aD=4Z2|gI`8k{D13um@bk|WTteUJBI+Yb&>D(C3}P^i%Pj0bKdch< z!T@v@EI{YORTzX@&;!fR2Vdb3e1|>o8#;U1+lVHiA3FOM;TViSn$3Li(7X_)VB?H$ zZ*tAz5f(LLLgNg>2#;F25w>*GU|LKFZfJ34m=@CwCc+kvvUPofmzI?kMV*?Nm@eJ8 z;!3c2blfNwu~9zQ^?h6B?Apsyt#T_LFJ*qOd6km+^ZH3jo)zTtf!xj2Pw(V~-6&sw KRFa1yYV{YqJ6~l0 delta 283 zcmYk#y$eB67{~GFa8ur2GEnl8Oe`eIiqWLIT0~jyt($>KiOnLJ%qAlne}u)zU%+aV z?*zdp}%UY@h|&waB;%-9MmNK#~>o4@s>j*u70k`IZKj1oy>6@A!6eXoZ_v@wjk zp9k#FpD~GE7c)d=lBZa)*~9<`SGPz4@2Cg7QGetyMEqF57?v@IEsWw2_4^6daEW@( p1xt8CY1)=G=#NLXm2EqY?Z_si%;R2}%BVN0mE*@XRhyG>{{rVVB9;IE diff --git a/plugin/translations/zh/LC_MESSAGES/messages.po b/plugin/translations/zh/LC_MESSAGES/messages.po index f4197ee..a620df0 100644 --- a/plugin/translations/zh/LC_MESSAGES/messages.po +++ b/plugin/translations/zh/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: 2.0.0\n" "Report-Msgid-Bugs-To: deefrawley@gmail.com\n" -"POT-Creation-Date: 2022-04-22 17:42+1000\n" +"POT-Creation-Date: 2022-04-22 18:59+1000\n" "PO-Revision-Date: 2020-12-13 20:56+1100\n" "Last-Translator: CitizenDee \n" "Language: zh\n" @@ -18,67 +18,72 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.1\n" -#: plugin/currency_converter.py:66 +#: plugin/currency_converter.py:68 #, fuzzy msgid " " msgstr "<热键> <量> <来源货币> <目标货币>" -#: plugin/currency_converter.py:67 +#: plugin/currency_converter.py:69 msgid "" "There will be a short delay if the currency rates file needs to be " "downloaded" msgstr "如果需要下载汇率文件,会有短暂的延迟" -#: plugin/currency_converter.py:76 +#: plugin/currency_converter.py:78 msgid "Source currency" msgstr "来源货币" -#: plugin/currency_converter.py:83 +#: plugin/currency_converter.py:85 msgid "Destination currency" msgstr "目的地货币" -#: plugin/currency_converter.py:89 +#: plugin/currency_converter.py:91 msgid "Please enter three character currency codes" msgstr "输入三个字符的货币代码" -#: plugin/currency_converter.py:94 +#: plugin/currency_converter.py:96 #, fuzzy msgid "Error - source is not a valid currency" msgstr "错误 - 来源货币不是有效货币" -#: plugin/currency_converter.py:99 +#: plugin/currency_converter.py:101 #, fuzzy msgid "Error - destination is not a valid currency" msgstr "错误 - 目的地货币不是有效货币" -#: plugin/currency_converter.py:102 +#: plugin/currency_converter.py:108 msgid "Error - amount must be numeric" msgstr "错误 - 金额必须是数字" -#: plugin/currency_converter.py:142 +#: plugin/currency_converter.py:152 msgid "Rates date : {}" msgstr "汇率日期 : {}" -#: plugin/currency_converter.py:146 +#: plugin/currency_converter.py:156 msgid "Couldn't download the rates file" msgstr "无法下载汇率文件" -#: plugin/currency_converter.py:147 +#: plugin/currency_converter.py:157 msgid "{} - check log for more details" msgstr "{} - 查看日志以获取更多详细信息" -#: plugin/currency_converter.py:201 +#: plugin/currency_converter.py:210 msgid "HTTP Error" msgstr "HTTP 错误" -#: plugin/currency_converter.py:204 +#: plugin/currency_converter.py:213 msgid "Connection Error" msgstr "连接错误" -#: plugin/currency_converter.py:207 +#: plugin/currency_converter.py:216 msgid "Unspecifed Download Error" msgstr "未指定的下载错误" -#: plugin/currency_converter.py:248 +#: plugin/currency_converter.py:228 +msgid "Error - expected source or destination currency not in rates file" +msgstr "错误 - 汇率文件中没有预期的来源或目标货币" + +#: plugin/currency_converter.py:266 msgid "No matches found" msgstr "未找到匹配项" +