Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
JakubFranek committed Jan 15, 2024
2 parents aea9840 + f245dc5 commit 69412cf
Show file tree
Hide file tree
Showing 73 changed files with 1,254 additions and 447 deletions.
2 changes: 1 addition & 1 deletion installer/installer_setup.iss
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#define MyAppName "Kapytal"
#define MyAppVersion "0.14.0"
#define MyAppVersion "0.15.0"
#define MyAppPublisher "Jakub Franek"
#define MyAppURL "https://github.com/JakubFranek/Kapytal"
#define MyAppExeName "Kapytal.exe"
Expand Down
5 changes: 3 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ def main() -> None:

app.processEvents() # draw MainView so WelcomeDialog can be properly centered

logging.debug("Checking for updates")
main_presenter.check_for_updates()
if user_settings.settings.check_for_updates_on_startup:
logging.debug("Checking for updates")
main_presenter.check_for_updates()

logging.debug("Showing Welcome dialog")
main_presenter.show_welcome_dialog()
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ extend-ignore = [
]

[tool.ruff.per-file-ignores]
"tests/*" = ["S101", "ANN401", "SLF001", "PLR2004"]
"tests/*" = ["S101", "ANN401", "SLF001", "PLR2004", "FBT001"]
"src/views/*" = ["FBT003", "N802"]
"src/view_models/*" = ["N802"]
2 changes: 0 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ altgraph==0.17.4
appdirs==1.4.4
attrs==23.1.0
beautifulsoup4==4.12.2
black==23.12.1
certifi==2023.11.17
charset-normalizer==3.3.2
click==8.1.7
Expand All @@ -20,7 +19,6 @@ iniconfig==2.0.0
kiwisolver==1.4.5
lxml==4.9.4
matplotlib==3.8.2
mplcursors==0.5.2
multitasking==0.0.11
mypy==1.8.0
mypy-extensions==1.0.0
Expand Down
6 changes: 5 additions & 1 deletion src/models/base_classes/transaction.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod
from collections.abc import Collection
from datetime import datetime
from datetime import date, datetime
from typing import TYPE_CHECKING

from src.models.custom_exceptions import NotFoundError
Expand Down Expand Up @@ -54,6 +54,10 @@ def _validate_description(self, value: str) -> None:
def datetime_(self) -> datetime:
return self._datetime

@property
def date_(self) -> date:
return self._datetime.date()

@property
def timestamp(self) -> float:
return self._timestamp
Expand Down
2 changes: 1 addition & 1 deletion src/models/model_objects/cash_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def balances(self) -> tuple[CashAmount, ...]:

def get_balance(self, currency: Currency, date_: date | None = None) -> CashAmount:
if date_ is None:
amount = self._balance_history[-1][1]
amount = self._balance_history[-1][1].convert(currency)
else:
index = bisect_right(
self._balance_history, date_, key=lambda x: x[0].date()
Expand Down
69 changes: 43 additions & 26 deletions src/models/model_objects/currency_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
from src.models.mixins.copyable_mixin import CopyableMixin
from src.models.mixins.json_serializable_mixin import JSONSerializableMixin
from src.models.user_settings import user_settings
from src.presenters.utilities.event import Event

# TODO: add CurrencyManager class to take care of Currency cache resets
# and offload RecordKeeper methods to CurrencyManager

quantizers: dict[int, Decimal] = {}
for i in range(18 + 1):
Expand All @@ -26,10 +30,16 @@ class ConversionFactorNotFoundError(ValueError):


class Currency(CopyableMixin, JSONSerializableMixin):
__slots__ = ("_code", "_places", "_exchange_rates", "_factor_cache", "_zero_amount")
__slots__ = (
"_code",
"_decimals",
"_exchange_rates",
"_factor_cache",
"_zero_amount",
)
CODE_LENGTH = 3

def __init__(self, code: str, places: int) -> None:
def __init__(self, code: str, decimals: int) -> None:
super().__init__()

if not isinstance(code, str):
Expand All @@ -38,11 +48,11 @@ def __init__(self, code: str, places: int) -> None:
raise ValueError("Currency.code must be a three letter ISO-4217 code.")
self._code = code.upper()

if not isinstance(places, int):
raise TypeError("Currency.places must be an integer.")
if places < 0:
raise ValueError("Currency.places must not be negative.")
self._places = places
if not isinstance(decimals, int):
raise TypeError("Currency.decimals must be an integer.")
if decimals < 0:
raise ValueError("Currency.decimals must not be negative.")
self._decimals = decimals

self._exchange_rates: dict[Currency, "ExchangeRate"] = {}
self._factor_cache: dict[str, Decimal] = {}
Expand All @@ -53,8 +63,8 @@ def code(self) -> str:
return self._code

@property
def places(self) -> int:
return self._places
def decimals(self) -> int:
return self._decimals

@property
def zero_amount(self) -> "CashAmount":
Expand Down Expand Up @@ -83,6 +93,9 @@ def __eq__(self, __o: object) -> bool:
return self._code == __o._code

def add_exchange_rate(self, exchange_rate: "ExchangeRate") -> None:
"""The class managing Currencies must reset all Currency caches
after this method is called."""

if not isinstance(exchange_rate, ExchangeRate):
raise TypeError("Parameter 'exchange_rate' must be an ExchangeRate.")
if self not in exchange_rate.currencies:
Expand All @@ -92,14 +105,15 @@ def add_exchange_rate(self, exchange_rate: "ExchangeRate") -> None:
)
other_currency = exchange_rate.currencies - {self}
self._exchange_rates[other_currency.pop()] = exchange_rate
self.reset_cache()

def remove_exchange_rate(self, exchange_rate: "ExchangeRate") -> None:
"""The class managing Currencies must reset all Currency caches
after this method is called."""

if not isinstance(exchange_rate, ExchangeRate):
raise TypeError("Parameter 'exchange_rate' must be an ExchangeRate.")
other_currency = exchange_rate.currencies - {self}
del self._exchange_rates[other_currency.pop()]
self.reset_cache()

def reset_cache(self) -> None:
self._factor_cache = {}
Expand Down Expand Up @@ -160,14 +174,13 @@ def _get_exchange_rates(
# Direct ExchangeRate found!
return [current_currency.exchange_rates[target_currency]]

# Direct ExchangeRate not found...
# Get unexplored currencies to iterate over.
# Direct ExchangeRate not found...get unexplored currencies to iterate over
iterable_currencies = [
currency
for currency in current_currency.convertible_to
if currency not in ignore_currencies
]
# Ignore these currencies in future deeper searches (no need to go back).
# Ignore these currencies in future deeper searches (no need to go back)
ignore_currencies = ignore_currencies | current_currency.convertible_to
for loop_currency in iterable_currencies:
exchange_rates = Currency._get_exchange_rates(
Expand All @@ -176,17 +189,21 @@ def _get_exchange_rates(
if exchange_rates is None:
continue # Reached a dead end.
# ExchangeRate to target_currency found!
# Append ExchangeRate needed to get there from the current_currency.
# Append ExchangeRate needed to get there from the current_currency
exchange_rates.insert(0, current_currency.exchange_rates[loop_currency])
return exchange_rates
return None # Reached a dead-end.

def serialize(self) -> dict:
return {"datatype": "Currency", "code": self._code, "places": self._places}
return {
"datatype": "Currency",
"code": self._code,
"places": self._decimals, # TODO: rename key
}

@staticmethod
def deserialize(data: dict[str, Any]) -> "Currency":
return Currency(code=data["code"], places=data["places"])
return Currency(code=data["code"], decimals=data["places"])


class ExchangeRate(CopyableMixin, JSONSerializableMixin):
Expand All @@ -200,6 +217,7 @@ class ExchangeRate(CopyableMixin, JSONSerializableMixin):
"_latest_date",
"_earliest_date",
"_recalculate_rate_history_pairs",
"event_reset_currency_caches",
)

def __init__(
Expand All @@ -222,6 +240,8 @@ def __init__(
self._rate_decimals = 0
self._recalculate_rate_history_pairs = False

self.event_reset_currency_caches = Event()

@property
def primary_currency(self) -> Currency:
return self._primary_currency
Expand Down Expand Up @@ -326,10 +346,8 @@ def delete_rate(self, date_: date, *, update: bool = True) -> None:
self.update_values()

def prepare_for_deletion(self) -> None:
self.primary_currency.remove_exchange_rate(self)
self.primary_currency.reset_cache()
self.secondary_currency.remove_exchange_rate(self)
self.secondary_currency.reset_cache()
self._primary_currency.remove_exchange_rate(self)
self._secondary_currency.remove_exchange_rate(self)

def calculate_return(
self, start: date | None = None, end: date | None = None
Expand Down Expand Up @@ -413,8 +431,7 @@ def update_values(self) -> None:
default=0,
)

self.primary_currency.reset_cache()
self.secondary_currency.reset_cache()
self.event_reset_currency_caches()
self._recalculate_rate_history_pairs = True


Expand Down Expand Up @@ -455,8 +472,8 @@ def value_rounded(self) -> Decimal:
if self._raw_value.is_nan():
self._value_rounded = self._raw_value
else:
self._value_rounded = round(self._raw_value, self._currency.places)
min_places = min(self._currency.places, 4)
self._value_rounded = round(self._raw_value, self._currency.decimals)
min_places = min(self._currency.decimals, 4)
if -self._value_rounded.as_tuple().exponent > min_places:
self._value_rounded = self._value_rounded.normalize()
if -self._value_rounded.as_tuple().exponent < min_places:
Expand All @@ -470,7 +487,7 @@ def value_rounded(self) -> Decimal:
def value_normalized(self) -> Decimal:
if not hasattr(self, "_value_normalized"):
self._value_normalized = self._raw_value.normalize()
places = min(self._currency.places, 4)
places = min(self._currency.decimals, 4)
if (
not self._value_normalized.is_nan()
and -self._value_normalized.as_tuple().exponent < places
Expand Down
Loading

0 comments on commit 69412cf

Please sign in to comment.