Code quality
This commit is contained in:
parent
d501c4da9f
commit
a12fcacf77
|
@ -20,25 +20,37 @@ import abc
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
|
from collections.abc import Mapping
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import enforce_typing
|
import enforce_typing
|
||||||
|
|
||||||
from ._version import __version__ # noqa: F401
|
from ._version import __version__
|
||||||
|
|
||||||
|
__all__ = ['__version__']
|
||||||
|
|
||||||
|
|
||||||
def parse_attr_data(attr_data: str) -> dict[str, str | int]:
|
def parse_id_attr_key_value_pair(attr_datum: str) -> tuple[str, str | int] | None:
|
||||||
d = {}
|
attr_datum = attr_datum.strip()
|
||||||
if attr_data is None:
|
if attr_datum == '':
|
||||||
return d
|
return None
|
||||||
for s in attr_data.split(','):
|
(attr_key, attr_value) = attr_datum.split('=')
|
||||||
s = s.strip()
|
|
||||||
if s == '':
|
|
||||||
continue
|
|
||||||
(attr_key, attr_value) = s.split('=')
|
|
||||||
if re.match(r'^\d+$', attr_value):
|
if re.match(r'^\d+$', attr_value):
|
||||||
attr_value = int(attr_value)
|
attr_value = int(attr_value)
|
||||||
d[attr_key] = attr_value
|
return attr_key, attr_value
|
||||||
|
|
||||||
|
|
||||||
|
def parse_id_attr_data(attr_data: str) -> Mapping[str, str | int]:
|
||||||
|
if attr_data is None:
|
||||||
|
return {}
|
||||||
|
key_value_pairs_str = attr_data.split(',')
|
||||||
|
key_value_pairs = [parse_id_attr_key_value_pair(kvp) for kvp in key_value_pairs_str]
|
||||||
|
|
||||||
|
d = {}
|
||||||
|
for kvp in key_value_pairs:
|
||||||
|
if kvp is None:
|
||||||
|
continue
|
||||||
|
d[kvp[0]] = kvp[1]
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
@ -124,8 +136,10 @@ class Asset:
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def raw_short_name(self) -> str:
|
def raw_short_name(self) -> str:
|
||||||
"""A short name or ticker that does not nessisarily uniquely identify
|
"""Short name or ticker.
|
||||||
the asset, but may be commonly used within the industry."""
|
|
||||||
|
Does not nessisarily uniquely identify the asset, but may be commonly used within the industry.
|
||||||
|
"""
|
||||||
|
|
||||||
def to_string_id(self) -> str:
|
def to_string_id(self) -> str:
|
||||||
"""Formats the asset id using the fin_defs asset format."""
|
"""Formats the asset id using the fin_defs asset format."""
|
||||||
|
@ -146,13 +160,12 @@ class Asset:
|
||||||
if isinstance(self, UnknownAsset):
|
if isinstance(self, UnknownAsset):
|
||||||
return 'unknown:XXX'
|
return 'unknown:XXX'
|
||||||
|
|
||||||
msg = f'Unsupported asset type: {repr(self)}'
|
msg = f'Unsupported asset type: {self:?}'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_string_id(asset_id: str, shortcut_well_known=True) -> 'Asset':
|
def from_string_id(asset_id: str) -> 'Asset':
|
||||||
"""Parses an `Asset` using the fin_defs asset format."""
|
"""Parses an `Asset` using the fin_defs asset format."""
|
||||||
|
|
||||||
m = re.match(r'^(?:(\w+):)?([^{]+)(?:\{(.*)\})?$', asset_id)
|
m = re.match(r'^(?:(\w+):)?([^{]+)(?:\{(.*)\})?$', asset_id)
|
||||||
if m is None:
|
if m is None:
|
||||||
msg = f'Unsupported asset format: {asset_id}'
|
msg = f'Unsupported asset format: {asset_id}'
|
||||||
|
@ -165,7 +178,7 @@ class Asset:
|
||||||
if known_symbol := WELL_KNOWN_SYMBOLS.get(ticker, None):
|
if known_symbol := WELL_KNOWN_SYMBOLS.get(ticker, None):
|
||||||
return known_symbol
|
return known_symbol
|
||||||
|
|
||||||
attrs: dict[str, str | int] = parse_attr_data(attr_data)
|
attrs: dict[str, str | int] = parse_id_attr_data(attr_data)
|
||||||
|
|
||||||
if prefix == 'stock':
|
if prefix == 'stock':
|
||||||
if m := re.match(r'^(\w+)(?:\.(\w+))?$', ticker):
|
if m := re.match(r'^(\w+)(?:\.(\w+))?$', ticker):
|
||||||
|
@ -218,7 +231,7 @@ class FiatCurrency(Currency):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_currency_symbol(symbol: str) -> 'Currency | None':
|
def from_currency_symbol(symbol: str) -> 'Currency | None':
|
||||||
for (currency, currency_symbol) in CURRENCY_SYMBOLS.items():
|
for currency, currency_symbol in CURRENCY_SYMBOLS.items():
|
||||||
if currency_symbol == symbol:
|
if currency_symbol == symbol:
|
||||||
return currency
|
return currency
|
||||||
return None
|
return None
|
||||||
|
@ -226,6 +239,7 @@ class FiatCurrency(Currency):
|
||||||
def to_currency_symbol(self) -> str:
|
def to_currency_symbol(self) -> str:
|
||||||
return CURRENCY_SYMBOLS[self]
|
return CURRENCY_SYMBOLS[self]
|
||||||
|
|
||||||
|
|
||||||
FiatCurrency.DKK = FiatCurrency('DKK')
|
FiatCurrency.DKK = FiatCurrency('DKK')
|
||||||
FiatCurrency.NOK = FiatCurrency('NOK')
|
FiatCurrency.NOK = FiatCurrency('NOK')
|
||||||
FiatCurrency.USD = FiatCurrency('USD')
|
FiatCurrency.USD = FiatCurrency('USD')
|
||||||
|
@ -233,6 +247,7 @@ FiatCurrency.EUR = FiatCurrency('EUR')
|
||||||
FiatCurrency.GBP = FiatCurrency('GBP')
|
FiatCurrency.GBP = FiatCurrency('GBP')
|
||||||
FiatCurrency.JPY = FiatCurrency('JPY')
|
FiatCurrency.JPY = FiatCurrency('JPY')
|
||||||
|
|
||||||
|
|
||||||
@enforce_typing.enforce_types
|
@enforce_typing.enforce_types
|
||||||
@dataclasses.dataclass(frozen=True, eq=True)
|
@dataclasses.dataclass(frozen=True, eq=True)
|
||||||
class CryptoCurrency(Currency):
|
class CryptoCurrency(Currency):
|
||||||
|
@ -240,11 +255,9 @@ class CryptoCurrency(Currency):
|
||||||
coingecko_id: str | None = None
|
coingecko_id: str | None = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
"""TODO
|
if not re.match('^.*$', self.ccxt_symbol):
|
||||||
if not re.match(RE_CRYPTO_TICKER_FORMAT, self.ccxt_symbol):
|
|
||||||
msg = f'ccxt_symbol was not in correct format: {self.ccxt_symbol}'
|
msg = f'ccxt_symbol was not in correct format: {self.ccxt_symbol}'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
"""
|
|
||||||
|
|
||||||
def raw_short_name(self) -> str:
|
def raw_short_name(self) -> str:
|
||||||
return self.ccxt_symbol
|
return self.ccxt_symbol
|
||||||
|
@ -295,7 +308,7 @@ class Commodity(Asset):
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
def raw_short_name(self) -> str:
|
def raw_short_name(self) -> str:
|
||||||
return self.alpha_vantage_id # TODO
|
return self.alpha_vantage_id
|
||||||
|
|
||||||
|
|
||||||
Commodity.CRUDE_OIL_WTI = Commodity('WTI')
|
Commodity.CRUDE_OIL_WTI = Commodity('WTI')
|
||||||
|
@ -485,6 +498,9 @@ class AssetInformation:
|
||||||
# TODO: FIGI types? (https://www.openfigi.com/)
|
# TODO: FIGI types? (https://www.openfigi.com/)
|
||||||
|
|
||||||
|
|
||||||
|
THREE_DECIMALS_UNDER_THIS_AMOUNT = 0.10
|
||||||
|
|
||||||
|
|
||||||
@enforce_typing.enforce_types
|
@enforce_typing.enforce_types
|
||||||
@dataclasses.dataclass(frozen=True, eq=True, slots=True)
|
@dataclasses.dataclass(frozen=True, eq=True, slots=True)
|
||||||
class AssetAmount:
|
class AssetAmount:
|
||||||
|
@ -502,10 +518,12 @@ class AssetAmount:
|
||||||
return self.human_readable_str()
|
return self.human_readable_str()
|
||||||
|
|
||||||
def human_readable_str(self) -> str:
|
def human_readable_str(self) -> str:
|
||||||
specificity = '2' if self.amount >= 0.10 else '3'
|
specificity = '2' if self.amount >= THREE_DECIMALS_UNDER_THIS_AMOUNT else '3'
|
||||||
prefix = ASSET_PREFIX.get(self.asset, '')
|
prefix = ASSET_PREFIX.get(self.asset, '')
|
||||||
return ('{}{:.' + specificity + 'f} {}').format(
|
return ('{}{:.' + specificity + 'f} {}').format(
|
||||||
prefix, self.amount, self.asset.raw_short_name(),
|
prefix,
|
||||||
|
self.amount,
|
||||||
|
self.asset.raw_short_name(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __mul__(self, other: Decimal) -> 'AssetAmount':
|
def __mul__(self, other: Decimal) -> 'AssetAmount':
|
||||||
|
@ -584,6 +602,7 @@ class AssetAmount:
|
||||||
def __ge__(self, other: 'AssetAmount') -> bool:
|
def __ge__(self, other: 'AssetAmount') -> bool:
|
||||||
return self.cmp(other) >= 0
|
return self.cmp(other) >= 0
|
||||||
|
|
||||||
|
|
||||||
AssetAmount.ZERO = AssetAmount(UnknownAsset(), Decimal(0))
|
AssetAmount.ZERO = AssetAmount(UnknownAsset(), Decimal(0))
|
||||||
|
|
||||||
|
|
||||||
|
@ -611,7 +630,10 @@ class ExchangeRateSample:
|
||||||
return self._average
|
return self._average
|
||||||
return (self.high + self.low) / 2
|
return (self.high + self.low) / 2
|
||||||
|
|
||||||
def replace_timestamp(self, timestamp: datetime.date | datetime.datetime) -> 'ExchangeRateSample':
|
def replace_timestamp(
|
||||||
|
self,
|
||||||
|
timestamp: datetime.date | datetime.datetime,
|
||||||
|
) -> 'ExchangeRateSample':
|
||||||
return dataclasses.replace(self, timestamp=timestamp)
|
return dataclasses.replace(self, timestamp=timestamp)
|
||||||
|
|
||||||
def invert_exchange_rate(self) -> 'ExchangeRateSample':
|
def invert_exchange_rate(self) -> 'ExchangeRateSample':
|
||||||
|
|
|
@ -13,7 +13,7 @@ def test_valid_tickers(ticker: str):
|
||||||
|
|
||||||
@pytest.mark.parametrize('ticker', BAD_TICKERS)
|
@pytest.mark.parametrize('ticker', BAD_TICKERS)
|
||||||
def test_bad_tickers(ticker: str):
|
def test_bad_tickers(ticker: str):
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError, match='ticker was not in correct format:.*'):
|
||||||
fin_defs.Stock(ticker, exchange=fin_defs.EXCHANGES_BY_IDS['NYSE'])
|
fin_defs.Stock(ticker, exchange=fin_defs.EXCHANGES_BY_IDS['NYSE'])
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,5 +23,5 @@ def test_crypto_tickers(ticker):
|
||||||
|
|
||||||
|
|
||||||
def test_str():
|
def test_str():
|
||||||
NVO = fin_defs.Stock('NVO', fin_defs.EXCHANGES_BY_IDS['NYSE'])
|
asset = fin_defs.Stock('NVO', fin_defs.EXCHANGES_BY_IDS['NYSE'])
|
||||||
assert str(NVO) == 'stock:NVO.XNYS'
|
assert str(asset) == 'stock:NVO.XNYS'
|
||||||
|
|
|
@ -22,13 +22,14 @@ INVALID_IDS = [
|
||||||
'commodity:TEST:TEST',
|
'commodity:TEST:TEST',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_parse_attr():
|
def test_parse_attr():
|
||||||
assert fin_defs.parse_attr_data('') == {}
|
assert fin_defs.parse_id_attr_data('') == {}
|
||||||
assert fin_defs.parse_attr_data(' ') == {}
|
assert fin_defs.parse_id_attr_data(' ') == {}
|
||||||
assert fin_defs.parse_attr_data('abc=abc') == {'abc': 'abc'}
|
assert fin_defs.parse_id_attr_data('abc=abc') == {'abc': 'abc'}
|
||||||
assert fin_defs.parse_attr_data('abc=123') == {'abc': 123}
|
assert fin_defs.parse_id_attr_data('abc=123') == {'abc': 123}
|
||||||
assert fin_defs.parse_attr_data('abc=123,xyz=abc') == {'abc': 123, 'xyz': 'abc'}
|
assert fin_defs.parse_id_attr_data('abc=123,xyz=abc') == {'abc': 123, 'xyz': 'abc'}
|
||||||
assert fin_defs.parse_attr_data(' abc=123 , xyz=abc ') == {
|
assert fin_defs.parse_id_attr_data(' abc=123 , xyz=abc ') == {
|
||||||
'abc': 123,
|
'abc': 123,
|
||||||
'xyz': 'abc',
|
'xyz': 'abc',
|
||||||
}
|
}
|
||||||
|
@ -43,38 +44,28 @@ def test_from_nordnet():
|
||||||
|
|
||||||
@pytest.mark.parametrize('asset_id', VALID_IDS)
|
@pytest.mark.parametrize('asset_id', VALID_IDS)
|
||||||
def test_from_string_id_shortcut(asset_id: str):
|
def test_from_string_id_shortcut(asset_id: str):
|
||||||
assert (
|
assert fin_defs.Asset.from_string_id(asset_id)
|
||||||
fin_defs.Asset.from_string_id(asset_id, shortcut_well_known=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('asset_id', VALID_IDS)
|
@pytest.mark.parametrize('asset_id', VALID_IDS)
|
||||||
def test_from_string_id(asset_id: str):
|
def test_from_string_id(asset_id: str):
|
||||||
assert (
|
assert fin_defs.Asset.from_string_id(asset_id)
|
||||||
fin_defs.Asset.from_string_id(asset_id, shortcut_well_known=False)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('asset_id', INVALID_IDS)
|
@pytest.mark.parametrize('asset_id', INVALID_IDS)
|
||||||
def test_from_string_id_invalid(asset_id: str):
|
def test_from_string_id_invalid(asset_id: str):
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(ValueError, match='.*format.*'):
|
||||||
fin_defs.Asset.from_string_id(asset_id, shortcut_well_known=False)
|
fin_defs.Asset.from_string_id(asset_id)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('asset', fin_defs.WELL_KNOWN_SYMBOLS.values())
|
@pytest.mark.parametrize('asset', fin_defs.WELL_KNOWN_SYMBOLS.values())
|
||||||
def test_to_from_string_id_shortcut(asset: fin_defs.Asset):
|
def test_to_from_string_id_shortcut(asset: fin_defs.Asset):
|
||||||
assert (
|
assert fin_defs.Asset.from_string_id(asset.to_string_id()) == asset
|
||||||
fin_defs.Asset.from_string_id(asset.to_string_id(), shortcut_well_known=True)
|
|
||||||
== asset
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('asset', fin_defs.WELL_KNOWN_SYMBOLS.values())
|
@pytest.mark.parametrize('asset', fin_defs.WELL_KNOWN_SYMBOLS.values())
|
||||||
def test_to_from_string_id(asset: fin_defs.Asset):
|
def test_to_from_string_id(asset: fin_defs.Asset):
|
||||||
assert (
|
assert fin_defs.Asset.from_string_id(asset.to_string_id()) == asset
|
||||||
fin_defs.Asset.from_string_id(asset.to_string_id(), shortcut_well_known=False)
|
|
||||||
== asset
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('asset', fin_defs.WELL_KNOWN_SYMBOLS.values())
|
@pytest.mark.parametrize('asset', fin_defs.WELL_KNOWN_SYMBOLS.values())
|
||||||
|
|
Loading…
Reference in New Issue
Block a user