Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Monetary API, Various Chores #19

Merged
merged 28 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
95ff743
refactor: use PEP484-compatible type hint
vst Jun 13, 2023
a00a403
fix: use more common names for pre-defined currencies
vst Jun 13, 2023
a029cb8
fix: correct spelling for some documentation and exception messages
vst Jun 13, 2023
2a26ead
chore: gitignore /.vscode directory
vst Jun 14, 2023
a24042b
test: replace tox with nox
vst Jun 14, 2023
67695df
test: add pyright to our test suite
vst Jun 14, 2023
7977dd4
fix: fix Protocol definitions
vst Jun 14, 2023
a705315
fix: make NonePrice.money a property
vst Jun 14, 2023
c038682
test: silence pyright error for false positive
vst Jun 14, 2023
62b97d4
feat: add {Money,Price}.qty_or_zero method
vst Jun 14, 2023
ae9c480
feat: add {Money,Price}.qty_or_none method
vst Jun 14, 2023
081b335
feat: add {Money,Price}.qty_or_else method
vst Jun 14, 2023
f859908
feat: add {Money,Price}.qty_map method
vst Jun 14, 2023
cffe534
feat: add {Money,Price}.fmap method
vst Jun 14, 2023
0f10309
feat: add {Money,Price}.dimap method
vst Jun 14, 2023
a6c25d1
feat: add {Money,Price}.ccy_or_none method
vst Jun 14, 2023
0ee64ab
feat: add {Money,Price}.dov_or_none method
vst Jun 14, 2023
d7085cb
feat: add {Money,Price}.or_else method
vst Jun 14, 2023
db1e716
test: enable pylint tests
vst Jun 14, 2023
de6ac3e
refactor: attend pylint check errors (invalid-name)
vst Jun 14, 2023
8beec42
refactor: attend various pylint check errors
vst Jun 14, 2023
c13334f
refactor: remove unnecessary abstractmethod body
vst Jun 14, 2023
359f4f5
refactor: make Money and Price abstract base class
vst Jun 14, 2023
11a077c
refactor: make {Money,Price}.{defined,undefined} properties
vst Jun 14, 2023
c79b092
refactor!: make {Money,Price}.NA a classmethod, rename to .na
vst Jun 14, 2023
c98b60d
refactor!: drop {Money,Price}.{ccy,qty,dov} type hints
vst Jun 14, 2023
693916d
test: reconfigure pylint
vst Jun 14, 2023
98f8259
feat: add type guards for {None,Some}{Money,Price} type checks
vst Jun 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ jobs:
extra_nix_config: "system-features = benchmark, big-parallel, nixos-test, uid-range, kvm"

- name: Run Tests
run: nix-shell --argstr python ${{ matrix.python-version }} --run tox
run: nix-shell --argstr python ${{ matrix.python-version }} --run "python -m nox"
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
.idea
.mypy_cache
.pytest_cache/
.tox
/.vscode
__pycache__
build
dist
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@ nix-shell
Run the test suite:

```sh
tox
python -m nox
```

> **Note:** Since we are under Nix shell, `nox` command will attempt to use its
> own Python interpreter pinned during `nox` installation. We want our own
> interpreter to be used during `nox` checks.

Alternatively:

```sh
nix-shell --argstr python python310 --run tox
nix-shell --argstr python python311 --run tox
nix-shell --argstr python python310 --run "python -m nox"
nix-shell --argstr python python311 --run "python -m nox"
```

## Publishing
Expand Down
5 changes: 4 additions & 1 deletion default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,14 @@ let
ps.ipython
ps.isort
ps.mypy
ps.nox
ps.pip
ps.pylint
ps.pytest
ps.pytest-cov
ps.sphinx
ps.sphinx-rtd-theme
ps.sphinxcontrib-apidoc
ps.tox
ps.twine
ps.wheel

Expand All @@ -127,6 +127,9 @@ let
packages = [
## Python dependency with packages for development purposes:
thisPythonWithDeps

## Further development dependencies:
pkgs.nodePackages.pyright
];

shellHook = ''
Expand Down
75 changes: 75 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""
This module provides the configuration for our complete test suite.

We are using `nox` as our test-suite runner.

Unlike the default and common use pattern, we let `nox` run all commands via the
same Python interpreter as it is invoked by.

This makes `pyright` happy. Furthermore, we are using Nix to install all
development dependencies anyway.

A few examples for invoking `nox`:

```sh
python -m nox
python -m nox -k "not pytest"
python -m nox -k "not pyright and not mypy and not pytest"
python -m nox -s pyright mypy
python -m nox -s black isort
```

Note that we are not invoking `nox` directly. The reason is that it is installed
via Nix and its Python interpreter is different than our Python interpreter
under the Nix shell. Otherwise, none of our development dependencies would be
accessible by `nox` sessions.
"""

import nox

#: Files and directories of interest.
paths = [
"noxfile.py",
"pypara",
"tests",
]


@nox.session(python=False)
def black(session: nox.Session) -> None:
session.run("black", "--check", *paths, external=True)


@nox.session(python=False)
def isort(session: nox.Session) -> None:
session.run("isort", "--check-only", *paths, external=True)


@nox.session(python=False)
def pylint(session: nox.Session) -> None:
session.run("pylint", "pypara", "tests", external=True)


@nox.session(python=False)
def flake8(session: nox.Session) -> None:
session.run("flake8", "pypara", "tests", external=True)


@nox.session(python=False)
def pyright(session: nox.Session) -> None:
session.run("pyright", external=True)


@nox.session(python=False)
def mypy(session: nox.Session) -> None:
session.run("mypy", "pypara", "tests", external=True)


@nox.session(python=False)
def pytest(session: nox.Session) -> None:
session.run("pytest", "--verbose", "--cov", "--doctest-modules", external=True)


@nox.session(python=False)
def piplist(session: nox.Session) -> None:
session.run("pip", "list", "-o")
2 changes: 1 addition & 1 deletion pypara/accounting/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,4 +397,4 @@ class ReadChartOfAccounts(Protocol):
"""

def __call__(self) -> COA:
pass
...
4 changes: 2 additions & 2 deletions pypara/accounting/journaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class JournalEntry(Generic[_T]):
@property
def increments(self) -> Iterable[Posting[_T]]:
"""
Incerement event postings of the journal entry.
Increment event postings of the journal entry.
"""
return (p for p in self.postings if p.direction == Direction.INC)

Expand Down Expand Up @@ -177,4 +177,4 @@ class ReadJournalEntries(Protocol[_T]):
"""

def __call__(self, period: DateRange) -> Iterable[JournalEntry[_T]]:
pass
...
4 changes: 2 additions & 2 deletions pypara/accounting/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ class ReadInitialBalances(Protocol):
"""

def __call__(self, period: DateRange) -> InitialBalances:
pass
...


class GeneralLedgerProgram(Protocol[_T]):
Expand All @@ -201,7 +201,7 @@ class GeneralLedgerProgram(Protocol[_T]):
"""

def __call__(self, period: DateRange) -> GeneralLedger[_T]:
pass
...


def compile_general_ledger_program(
Expand Down
4 changes: 2 additions & 2 deletions pypara/commons/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ def chunk(lst: List[_T], n: int) -> Iterable[List[_T]]:
>>> list(chunk([1, 2, 3, 4, 5], 2))
[[1, 2], [3, 4], [5]]
"""
for i in range(0, len(lst), n):
yield lst[i : i + n] # noqa: E203
for x in range(0, len(lst), n):
yield lst[x : x + n] # noqa: E203
33 changes: 18 additions & 15 deletions pypara/commons/zeitgeist.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@

class OpenDateRange:
"""
Defines a daterange class with inclusive but optional start and end.
Defines a date-range class with inclusive but optional start and end.

For a date range with both ends open:

Expand Down Expand Up @@ -248,14 +248,14 @@ def ordered(cls, date1: Date, date2: Date) -> "DateRange":
return cls(date1, date2) if date1 <= date2 else cls(date2, date1)

@classmethod
def pivotal(cls, pivot: Date, prev: NaturalNumber, next: NaturalNumber) -> "DateRange":
def pivotal(cls, pivot: Date, n_prev: NaturalNumber, n_next: NaturalNumber) -> "DateRange":
"""
Creates a :py:class:`DateRange` as per given ``pivot`` date in time to create date range since ``prev`` years
back and ``next`` years forward.

:param pivot: Pivot date in time.
:param prev: Numbers to go back in years.
:param next: Numbers to go forward in years.
:param n_prev: Numbers to go back in years.
:param n_next: Numbers to go forward in years.
:return: A :py:class:`DateRange` instance.

>>> DateRange.pivotal(Date(2019, 12, 31), NaturalNumber(0), NaturalNumber(0))
Expand All @@ -276,21 +276,21 @@ def pivotal(cls, pivot: Date, prev: NaturalNumber, next: NaturalNumber) -> "Date
DateRange(since=datetime.date(2019, 2, 28), until=datetime.date(2021, 2, 28))
"""
## Get the target since date:
sy, sm, sd = pivot.year - prev, pivot.month, pivot.day
s_y, s_m, s_d = pivot.year - n_prev, pivot.month, pivot.day

## Check if (m, d) is a valid one:
if not isleap(sy) and sm == 2 and sd == 29:
sd = 28
if not isleap(s_y) and s_m == 2 and s_d == 29:
s_d = 28

## Get the target until date:
uy, um, ud = pivot.year + next, pivot.month, pivot.day
u_y, u_m, u_d = pivot.year + n_next, pivot.month, pivot.day

## Check if (m, d) is a valid one:
if not isleap(uy) and um == 2 and ud == 29:
ud = 28
if not isleap(u_y) and u_m == 2 and u_d == 29:
u_d = 28

## Create the date range and return:
return cls(Date(sy, sm, sd), Date(uy, um, ud))
return cls(Date(s_y, s_m, s_d), Date(u_y, u_m, u_d))

@classmethod
def dtd(cls, date: Date) -> "DateRange":
Expand Down Expand Up @@ -367,10 +367,13 @@ def cover(cls, first: "DateRange", *rest: "DateRange") -> "DateRange":
:param rest: Rest of date ranges.
:return: A new date range which covers all given date ranges.

>>> DateRange.cover(DateRange.dtd(Date(2019, 4, 1)), DateRange.mtd(Date(2020, 2, 29)), DateRange.ytd(Date(2018, 3, 6))) # noqa: E501
>>> DateRange.cover(DateRange.dtd(Date(2019, 4, 1)), DateRange.mtd(Date(2020, 2, 29)), DateRange.ytd(Date(2018, 3, 6))) # noqa: E501 pylint: disable=line-too-long
DateRange(since=datetime.date(2018, 1, 1), until=datetime.date(2020, 2, 29))
"""
return DateRange(min(first, *rest, key=lambda x: x.since).since, max(first, *rest, key=lambda x: x.until).until)
return DateRange(
min(first, *rest, key=lambda x: x.since).since, # pyright: ignore [reportGeneralTypeIssues]
max(first, *rest, key=lambda x: x.until).until, # pyright: ignore [reportGeneralTypeIssues]
)

def since_prev_year_end(self, years: PositiveInteger = _POS_INT_1, weekday: bool = False) -> "DateRange":
"""
Expand Down Expand Up @@ -411,7 +414,7 @@ def make_financial_periods(date: Date, lookback: PositiveInteger) -> FinancialPe

:param date: Date to create financial periods for.
:param lookback: A positive integer describing the number of years to go back.
:return: A dictionary of financial period label and date range for the financual period.
:return: A dictionary of financial period label and date range for the financial period.

>>> sorted(make_financial_periods(Date(2019, 1, 10), PositiveInteger(1)).keys())
['2018', 'DTD', 'MTD', 'YTD']
Expand Down Expand Up @@ -810,7 +813,7 @@ def ensure_datetime(value: Union[Date, DateTime, str], **kwargs: int) -> DateTim
...
ValueError: Don't know how to convert value to date/time object: 1
"""
## Check the type of the value and act accordinly.
## Check the type of the value and act accordingly.
if isinstance(value, DateTime):
## It is a datetime instance. Nothing to be done. Just return with replacement:
return value.replace(**kwargs) # type: ignore
Expand Down
12 changes: 6 additions & 6 deletions pypara/currencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,15 +210,15 @@ class CurrencyRegistry:
"""

#: Defines the singleton instance.
__instance = None # type: CurrencyRegistry
__instance: "CurrencyRegistry" = None # type: ignore

def __new__(cls) -> "CurrencyRegistry":
"""
Creates the singleton instance, or returns the existing one.
"""
## Do we have the singleton instance?
if CurrencyRegistry.__instance is None:
## Nope, not yet. Creat one:
## Nope, not yet. Create one:
CurrencyRegistry.__instance = object.__new__(cls)

## Return the singleton instance.
Expand Down Expand Up @@ -373,7 +373,7 @@ def codenames(self) -> List[Tuple[str, str]]:
register(Currency.of("ARS", "Argentine Peso", 2, CurrencyType.MONEY))
register(Currency.of("AUD", "Australian Dollar", 2, CurrencyType.MONEY))
register(Currency.of("AWG", "Aruban Florin", 2, CurrencyType.MONEY))
register(Currency.of("AZN", "Azerbaijanian Manat", 2, CurrencyType.MONEY))
register(Currency.of("AZN", "Azerbaijani Manat", 2, CurrencyType.MONEY))
register(Currency.of("BAM", "Convertible Mark", 2, CurrencyType.MONEY))
register(Currency.of("BBD", "Barbados Dollar", 2, CurrencyType.MONEY))
register(Currency.of("BCH", "Bitcoin Cash", -1, CurrencyType.CRYPTO))
Expand All @@ -390,7 +390,7 @@ def codenames(self) -> List[Tuple[str, str]]:
register(Currency.of("BTC", "Bitcoin", -1, CurrencyType.CRYPTO))
register(Currency.of("BTN", "Ngultrum", 2, CurrencyType.MONEY))
register(Currency.of("BWP", "Pula", 2, CurrencyType.MONEY))
register(Currency.of("BYR", "Belarussian Ruble", 0, CurrencyType.MONEY))
register(Currency.of("BYR", "Belarusian Ruble", 0, CurrencyType.MONEY))
register(Currency.of("BZD", "Belize Dollar", 2, CurrencyType.MONEY))
register(Currency.of("CAD", "Canadian Dollar", 2, CurrencyType.MONEY))
register(Currency.of("CDF", "Congolese Franc", 2, CurrencyType.MONEY))
Expand All @@ -406,7 +406,7 @@ def codenames(self) -> List[Tuple[str, str]]:
register(Currency.of("CRC", "Costa Rican Colon", 2, CurrencyType.MONEY))
register(Currency.of("CUC", "Peso Convertible", 2, CurrencyType.MONEY))
register(Currency.of("CUP", "Cuban Peso", 2, CurrencyType.MONEY))
register(Currency.of("CVE", "Cabo Verde Escudo", 2, CurrencyType.MONEY))
register(Currency.of("CVE", "Cape Verdean Escudo", 2, CurrencyType.MONEY))
register(Currency.of("CZK", "Czech Koruna", 2, CurrencyType.MONEY))
register(Currency.of("DASH", "Dash", -1, CurrencyType.CRYPTO))
register(Currency.of("DJF", "Djibouti Franc", 0, CurrencyType.MONEY))
Expand Down Expand Up @@ -528,7 +528,7 @@ def codenames(self) -> List[Tuple[str, str]]:
register(Currency.of("USD", "US Dollar", 2, CurrencyType.MONEY))
register(Currency.of("USN", "US Dollar (Next day)", 2, CurrencyType.MONEY))
register(Currency.of("UYI", "Uruguay Peso en Unidades Indexadas", 0, CurrencyType.MONEY))
register(Currency.of("UYU", "Peso Uruguayo", 2, CurrencyType.MONEY))
register(Currency.of("UYU", "Uruguayan Peso", 2, CurrencyType.MONEY))
register(Currency.of("UZS", "Uzbekistan Sum", 2, CurrencyType.MONEY))
register(Currency.of("VEF", "Bolivar", 2, CurrencyType.MONEY))
register(Currency.of("VND", "Dong", 0, CurrencyType.MONEY))
Expand Down
12 changes: 6 additions & 6 deletions pypara/dcc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
This module provides day-count convention definitions and functionlity.
This module provides day-count convention definitions and functionality.
"""

__all__ = ["DCC", "DCCRegistry"]
Expand Down Expand Up @@ -36,8 +36,8 @@ def _get_date_range(start: Date, end: Date) -> Iterable[Date]:
:param end: The end date of the period.
:return: A generator of dates.
"""
for i in range((end - start).days):
yield start + datetime.timedelta(days=i)
for d in range((end - start).days):
yield start + datetime.timedelta(days=d)


def _get_actual_day_count(start: Date, end: Date) -> int:
Expand Down Expand Up @@ -340,7 +340,7 @@ def find(self, name: str) -> Optional[DCC]:
"""
Attempts to find the day count convention by the given name.

Note that all day count conventions are registered under stripped, uppercased names. Therefore,
Note that all day count conventions are registered under stripped, uppercase names. Therefore,
the implementation will first attempt to find by given name as is. If it can not find it, it will
strip and uppercase the name and try to find it as such as a last resort.
"""
Expand Down Expand Up @@ -430,7 +430,7 @@ def register_and_return_dcfc(func: DCFC) -> DCFC:
## Attach the dcc instance to the day count fraction calculation function (for whatever it is worth):
setattr(func, "__dcc", dcc) # noqa: B010

## Done, return the function (if above statment did not raise any exceptions):
## Done, return the function (if above statement did not raise any exceptions):
return func

return register_and_return_dcfc
Expand Down Expand Up @@ -831,7 +831,7 @@ def dcfc_30_360_us(start: Date, asof: Date, end: Date, freq: Optional[Decimal] =
d2 = 30

## Revisit d2:
if d2 == 31 and (d1 == 30 or d1 == 31):
if d2 == 31 and (d1 in {30, 31}):
d2 = 30

## Revisit d1:
Expand Down
4 changes: 1 addition & 3 deletions pypara/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,8 @@ def query(self, ccy1: Currency, ccy2: Currency, asof: Date, strict: bool = False
:param ccy2: The second currency of foreign exchange rate.
:param asof: Temporal dimension the foreign exchange rate is effective as of.
:param strict: Indicates if we should raise a lookup error if that the foreign exchange rate can not be found.
:return: The foreign exhange rate as a :class:`Decimal` instance or None.
:return: The foreign exchange rate as a :class:`Decimal` instance or None.
"""
pass

@abstractmethod
def queries(self, queries: Iterable[TQuery], strict: bool = False) -> Iterable[Optional[FXRate]]:
Expand All @@ -160,4 +159,3 @@ def queries(self, queries: Iterable[TQuery], strict: bool = False) -> Iterable[O
:param strict: Indicates if we should raise a lookup error if that the foreign exchange rate can not be found.
:return: An iterable of rates.
"""
pass
Loading