Skip to content
This repository has been archived by the owner on Jan 26, 2023. It is now read-only.

Commit

Permalink
feat: Added currency support (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
DenverCoder1 authored Jan 10, 2023
1 parent 48f5ec7 commit 85aa7c3
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 22 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![License MIT](https://custom-icon-badges.herokuapp.com/github/license/DenverCoder1/unit-converter-albert-ext.svg?logo=repo)](https://github.com/DenverCoder1/unit-converter-albert-ext/blob/main/LICENSE)
[![code style black](https://custom-icon-badges.herokuapp.com/badge/code%20style-black-black.svg?logo=black-b&logoColor=white)](https://github.com/psf/black)

Extension for converting units of length, mass, speed, temperature, time, current, luminosity, printing measurements, molecular substance, in [Albert launcher](https://albertlauncher.github.io/)
Extension for converting units of length, mass, speed, temperature, time, current, luminosity, printing measurements, molecular substance, currency, and more in [Albert launcher](https://albertlauncher.github.io/)

![demo](https://user-images.githubusercontent.com/20955511/147166860-2550fe42-ba6f-4ae6-a305-5e5ed26b606b.gif)

Expand Down Expand Up @@ -60,6 +60,8 @@ Examples:

`convert 3.14159 rad to degrees`

`convert 100 EUR to USD`

To configure the trigger to be something other than "convert ", open the `Triggers` tab in the Albert settings.

![configure trigger](https://user-images.githubusercontent.com/20955511/211632106-981ce5a8-0311-47d5-aefe-3ab9d669fc3f.png)
Expand Down
167 changes: 146 additions & 21 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@
import json
import re
import traceback
from datetime import datetime
from pathlib import Path
from typing import Any
from urllib.request import urlopen
from xml.etree import ElementTree

import albert
import pint
import inflect
import pint

default_trigger = "convert "
synopsis = "<amount> <from_unit> to <to_unit>"

__doc__ = f"""
Extension for converting units of length, mass, speed, temperature, time,
current, luminosity, printing measurements, molecular substance, and more
current, luminosity, printing measurements, molecular substance, currency, and more
Synopsis: {default_trigger}{synopsis}
Expand All @@ -27,12 +30,13 @@
`{default_trigger}88 mph to kph`
`{default_trigger}32 degrees F to C`
`{default_trigger}3.14159 rad to degrees`
`{default_trigger}100 USD to EUR`
"""

md_iid = "0.5"
md_version = "1.1"
md_version = "1.2"
md_name = "Unit Converter"
md_description = "Convert length, mass, speed, temperature, time, and more"
md_description = "Convert length, mass, temperature, time, currency, and more"
md_license = "MIT"
md_url = "https://github.com/DenverCoder1/unit-converter-albert-ext"
md_lib_dependencies = ["pint", "inflect"]
Expand Down Expand Up @@ -69,51 +73,56 @@ class ConversionResult:
def __init__(
self,
from_amount: float,
from_unit: pint.Unit,
from_unit: str,
to_amount: float,
to_unit: pint.Unit,
to_unit: str,
dimensionality: str,
):
"""
Initialize the ConversionResult
Args:
from_amount (float): The amount to convert from
from_unit (Unit): The unit to convert from
from_unit (str): The unit to convert from
to_amount (float): The resulting amount
to_unit (Unit): The unit converted to
to_unit (str): The unit converted to
dimensionality (str): The dimensionality of the result
"""
self.from_amount = from_amount
self.from_unit = from_unit
self.to_amount = to_amount
self.to_unit = to_unit
self.dimensionality = units._get_dimensionality(to_unit)
self.dimensionality = dimensionality
self.display_names = config.get("display_names", {})
self.rounding_precision = int(config.get("rounding_precision", 3))
self.rounding_precision_zero = int(config.get("rounding_precision_zero", 12))

def __pluralize_unit(self, unit: pint.Unit) -> str:
def __pluralize_unit(self, unit: str) -> str:
"""
Pluralize the unit
Args:
unit (Unit): The unit to pluralize
unit (str): The unit to pluralize
Returns:
str: The pluralized unit
"""
return inflect_engine.plural(str(unit))
# if all characters are uppercase, don't pluralize
if unit.isupper():
return unit
return inflect_engine.plural(unit)

def __display_unit_name(self, amount: float, unit: pint.Unit) -> str:
def __display_unit_name(self, amount: float, unit: str) -> str:
"""
Display the name of the unit with plural if necessary
Args:
unit (Unit): The unit to display
unit (str): The unit to display
Returns:
str: The name of the unit
"""
unit = self.__pluralize_unit(unit) if amount != 1 else str(unit)
unit = self.__pluralize_unit(unit) if amount != 1 else unit
return self.display_names.get(unit, unit)

def __format_float(self, num: float) -> str:
Expand Down Expand Up @@ -161,7 +170,7 @@ def icon(self) -> str:
Return the icon for the result's dimensionality
"""
# strip characters from the dimensionality if not alphanumeric or underscore
dimensionality = re.sub(r"[^\w]", "", str(self.dimensionality))
dimensionality = re.sub(r"[^\w]", "", self.dimensionality)
return f"{dimensionality}.svg"

def __repr__(self):
Expand Down Expand Up @@ -222,13 +231,119 @@ def convert_units(self, amount: float, from_unit: str, to_unit: str) -> Conversi
result = input_unit.to(output_unit)
return ConversionResult(
from_amount=float(amount),
from_unit=self._get_unit(from_unit),
from_unit=str(self._get_unit(from_unit)),
to_amount=result.magnitude,
to_unit=result.units,
to_unit=str(result.units),
dimensionality=str(units._get_dimensionality(result.units)),
)


class UnknownCurrencyError(Exception):
def __init__(self, currency: str):
"""
Initialize the UnknownCurrencyError
Args:
currency (str): The unknown currency
"""
self.currency = currency
super().__init__(f"Unknown currency: {currency}")


class CurrencyConverter:

API_URL = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"

def __init__(self):
"""
Initialize the CurrencyConverter
"""
self.last_update = datetime.now()
self.aliases: dict[str, str] = config.get("aliases", {})
self.currencies = self._get_currencies()

def _get_currencies(self) -> dict[str, float]:
"""
Get the currencies from the API
Returns:
dict[str, float]: The currencies
"""
with urlopen(self.API_URL) as response:
xml = response.read().decode("utf-8").strip()
root = ElementTree.fromstring(xml)
currency_cube = root[-1][0]
if currency_cube is None:
albert.warning("Could not find currencies in XML")
return {}
currencies = {
currency.attrib["currency"]: float(currency.attrib["rate"])
for currency in currency_cube
}
currencies["EUR"] = 1
albert.info(f"Loaded currencies: {currencies}")
return currencies

def normalize_currency(self, currency: str) -> str | None:
"""
Get the currency name normalized using aliases
Args:
currency (str): The currency to normalize
Returns:
Optional[str]: The currency name or None if not found
"""
currency = self.aliases.get(currency, currency).upper()
return currency if currency in self.currencies else None

def convert_currency(
self, amount: float, from_currency: str, to_currency: str
) -> ConversionResult:
"""
Convert a currency to another currency
Args:
amount (float): The amount to convert
from_currency (str): The currency to convert from
to_currency (str): The currency to convert to
Returns:
str: The resulting amount in the new currency
Raises:
UnknownCurrencyError: If the currency is not valid
"""
# update the currencies every 24 hours
if (datetime.now() - self.last_update).days >= 1:
self.currencies = self._get_currencies()
self.last_update = datetime.now()
# get the currency rates
from_unit = self.normalize_currency(from_currency)
to_unit = self.normalize_currency(to_currency)
# convert the currency
if from_unit is None:
raise UnknownCurrencyError(from_currency)
if to_unit is None:
raise UnknownCurrencyError(to_currency)
from_rate = self.currencies[from_unit]
to_rate = self.currencies[to_unit]
result = amount * to_rate / from_rate
return ConversionResult(
from_amount=float(amount),
from_unit=from_unit,
to_amount=result,
to_unit=to_unit,
dimensionality="currency",
)


class Plugin(albert.QueryHandler):
def initialize(self):
"""Initialize the plugin."""
self.unit_converter = UnitConverter()
self.currency_converter = CurrencyConverter()

def id(self) -> str:
return __name__

Expand Down Expand Up @@ -307,10 +422,16 @@ def get_items(self, amount: float, from_unit: str, to_unit: str) -> list[albert.
Returns:
List[albert.Item]: The list of items to display
"""
uc = UnitConverter()
try:
# convert the units
result = uc.convert_units(amount, from_unit, to_unit)
# convert currencies
if (
self.currency_converter.normalize_currency(from_unit) is not None
and self.currency_converter.normalize_currency(to_unit) is not None
):
result = self.currency_converter.convert_currency(amount, from_unit, to_unit)
# convert standard units
else:
result = self.unit_converter.convert_units(amount, from_unit, to_unit)
# return the result
return [
self.create_item(
Expand All @@ -329,3 +450,7 @@ def get_items(self, amount: float, from_unit: str, to_unit: str) -> list[albert.
albert.warning(f"UndefinedUnitError: {e}")
albert.warning(traceback.format_exc())
return []
except UnknownCurrencyError as e:
albert.warning(f"UnknownCurrencyError: {e}")
albert.warning(traceback.format_exc())
return []
11 changes: 11 additions & 0 deletions icons/currency.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 85aa7c3

Please sign in to comment.