rangekeeper 0.7.7__tar.gz → 0.7.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/PKG-INFO +16 -8
  2. rangekeeper-0.7.8/README.md +26 -0
  3. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/Rangekeeper.egg-info/PKG-INFO +16 -8
  4. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/pyproject.toml +1 -1
  5. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/flux.py +34 -14
  6. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/formula/financial.py +95 -52
  7. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_api.py +56 -50
  8. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_formulas.py +11 -7
  9. rangekeeper-0.7.7/README.md +0 -18
  10. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/Rangekeeper.egg-info/SOURCES.txt +0 -0
  11. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/Rangekeeper.egg-info/dependency_links.txt +0 -0
  12. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/Rangekeeper.egg-info/requires.txt +0 -0
  13. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/Rangekeeper.egg-info/top_level.txt +0 -0
  14. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/__init__.py +0 -0
  15. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/api.py +0 -0
  16. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/conftest.py +0 -0
  17. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/distribution.py +0 -0
  18. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/duration.py +0 -0
  19. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/dynamics/__init__.py +0 -0
  20. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/dynamics/black_swan.py +0 -0
  21. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/dynamics/cyclicality.py +0 -0
  22. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/dynamics/market.py +0 -0
  23. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/dynamics/noise.py +0 -0
  24. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/dynamics/trend.py +0 -0
  25. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/dynamics/volatility.py +0 -0
  26. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/extrapolation.py +0 -0
  27. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/format.py +0 -0
  28. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/formula/__init__.py +0 -0
  29. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/graph.py +0 -0
  30. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/measure.py +0 -0
  31. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/policy.py +0 -0
  32. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/projection.py +0 -0
  33. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/segmentation.py +0 -0
  34. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/rangekeeper/space.py +0 -0
  35. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/setup.cfg +0 -0
  36. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_dynamics.py +0 -0
  37. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_graph.py +0 -0
  38. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_measures.py +0 -0
  39. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_models.py +0 -0
  40. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_modules.py +0 -0
  41. {rangekeeper-0.7.7 → rangekeeper-0.7.8}/tests/test_projections.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Rangekeeper
3
- Version: 0.7.7
3
+ Version: 0.7.8
4
4
  Summary: A Python library assisting financial modelling in real estate asset & development planning, decision-making, cashflow forecasting, and scenario analysis.
5
5
  Author-email: Daniel Fink <danfink@mit.edu>
6
6
  License-Expression: MPL-2.0
@@ -30,16 +30,24 @@ This directory holds the source code for the Rangekeeper Library
30
30
 
31
31
  ## Installation
32
32
  The library is offered through PyPI, and so its installation in other projects can be performed by:
33
- `pip install rangekeeper` or `poetry add rangekeeper`
33
+ `pip install rangekeeper`, `poetry add rangekeeper`, or `uv add rangekeeper`, depending on your virtual environment manager.
34
34
 
35
35
  ## Development
36
- If you wish to contribute to its development, it is recommended to use [Poetry](https://python-poetry.org/) for environment and dependency management:
36
+ If you wish to contribute to its development, it is recommended to use [uv](https://docs.astral.sh/uv/) for
37
+ environment and dependency management:
37
38
 
38
39
  ### Environment Setup
39
40
 
40
- 1. Install poetry, if you haven't yet: <https://python-poetry.org/docs/master/#installing-with-the-official-installer>
41
+ 1. Install uv, if you haven't yet: <https://docs.astral.sh/uv/>
41
42
  2. Clone this repo.
42
- 3. Use a terminal to install poetry packages from the repo's directory: `<path_to_repo>$ poetry install`
43
- 4. If you wish to develop this repo alongside other projects locally, you may install the local Rangekeeper library via `<path_to_repo>$ poetry run pip install -e <path_to_rangekeeper_repo>`,
44
- replacing `<path_to_rangekeeper_repo>` with its location on your system.
45
- 5. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use the [Poetry Dotenv Plugin](https://github.com/mpeteuil/poetry-dotenv-plugin) via `poetry self add poetry-dotenv-plugin`, and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
43
+ 3. Create a virtual environment: `uv venv .venv`
44
+ 4. Activate the virtual environment: `source .venv/bin/activate`
45
+ 5. Install dependencies: `uv pip install -r <(uv pip compile pyproject.toml)`
46
+ 6. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use [Python-Dotenv]
47
+ (https://github.com/theskumar/python-dotenv), and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
48
+
49
+ ### Publishing
50
+ 1. First, remove any previously built packages: `rm -rf dist/*`
51
+ 2. Build the package: `uv build`
52
+ 3. Then, publish it to PyPI with your $UV_PUBLISH_TOKEN token recorded in a .env file: `uv publish`: export $(grep -v
53
+ '^#' .env | xargs) && uv publish --token $UV_PUBLISH_TOKEN
@@ -0,0 +1,26 @@
1
+ # Rangekeeper Source
2
+ This directory holds the source code for the Rangekeeper Library
3
+
4
+ ## Installation
5
+ The library is offered through PyPI, and so its installation in other projects can be performed by:
6
+ `pip install rangekeeper`, `poetry add rangekeeper`, or `uv add rangekeeper`, depending on your virtual environment manager.
7
+
8
+ ## Development
9
+ If you wish to contribute to its development, it is recommended to use [uv](https://docs.astral.sh/uv/) for
10
+ environment and dependency management:
11
+
12
+ ### Environment Setup
13
+
14
+ 1. Install uv, if you haven't yet: <https://docs.astral.sh/uv/>
15
+ 2. Clone this repo.
16
+ 3. Create a virtual environment: `uv venv .venv`
17
+ 4. Activate the virtual environment: `source .venv/bin/activate`
18
+ 5. Install dependencies: `uv pip install -r <(uv pip compile pyproject.toml)`
19
+ 6. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use [Python-Dotenv]
20
+ (https://github.com/theskumar/python-dotenv), and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
21
+
22
+ ### Publishing
23
+ 1. First, remove any previously built packages: `rm -rf dist/*`
24
+ 2. Build the package: `uv build`
25
+ 3. Then, publish it to PyPI with your $UV_PUBLISH_TOKEN token recorded in a .env file: `uv publish`: export $(grep -v
26
+ '^#' .env | xargs) && uv publish --token $UV_PUBLISH_TOKEN
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Rangekeeper
3
- Version: 0.7.7
3
+ Version: 0.7.8
4
4
  Summary: A Python library assisting financial modelling in real estate asset & development planning, decision-making, cashflow forecasting, and scenario analysis.
5
5
  Author-email: Daniel Fink <danfink@mit.edu>
6
6
  License-Expression: MPL-2.0
@@ -30,16 +30,24 @@ This directory holds the source code for the Rangekeeper Library
30
30
 
31
31
  ## Installation
32
32
  The library is offered through PyPI, and so its installation in other projects can be performed by:
33
- `pip install rangekeeper` or `poetry add rangekeeper`
33
+ `pip install rangekeeper`, `poetry add rangekeeper`, or `uv add rangekeeper`, depending on your virtual environment manager.
34
34
 
35
35
  ## Development
36
- If you wish to contribute to its development, it is recommended to use [Poetry](https://python-poetry.org/) for environment and dependency management:
36
+ If you wish to contribute to its development, it is recommended to use [uv](https://docs.astral.sh/uv/) for
37
+ environment and dependency management:
37
38
 
38
39
  ### Environment Setup
39
40
 
40
- 1. Install poetry, if you haven't yet: <https://python-poetry.org/docs/master/#installing-with-the-official-installer>
41
+ 1. Install uv, if you haven't yet: <https://docs.astral.sh/uv/>
41
42
  2. Clone this repo.
42
- 3. Use a terminal to install poetry packages from the repo's directory: `<path_to_repo>$ poetry install`
43
- 4. If you wish to develop this repo alongside other projects locally, you may install the local Rangekeeper library via `<path_to_repo>$ poetry run pip install -e <path_to_rangekeeper_repo>`,
44
- replacing `<path_to_rangekeeper_repo>` with its location on your system.
45
- 5. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use the [Poetry Dotenv Plugin](https://github.com/mpeteuil/poetry-dotenv-plugin) via `poetry self add poetry-dotenv-plugin`, and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
43
+ 3. Create a virtual environment: `uv venv .venv`
44
+ 4. Activate the virtual environment: `source .venv/bin/activate`
45
+ 5. Install dependencies: `uv pip install -r <(uv pip compile pyproject.toml)`
46
+ 6. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use [Python-Dotenv]
47
+ (https://github.com/theskumar/python-dotenv), and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
48
+
49
+ ### Publishing
50
+ 1. First, remove any previously built packages: `rm -rf dist/*`
51
+ 2. Build the package: `uv build`
52
+ 3. Then, publish it to PyPI with your $UV_PUBLISH_TOKEN token recorded in a .env file: `uv publish`: export $(grep -v
53
+ '^#' .env | xargs) && uv publish --token $UV_PUBLISH_TOKEN
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Rangekeeper"
3
- version = "0.7.7"
3
+ version = "0.7.8"
4
4
  description = "A Python library assisting financial modelling in real estate asset & development planning, decision-making, cashflow forecasting, and scenario analysis."
5
5
  authors = [
6
6
  { name = "Daniel Fink", email = "danfink@mit.edu" }
@@ -5,7 +5,7 @@ import itertools
5
5
  import json
6
6
  import locale
7
7
  import os
8
- from typing import Dict, Union, Optional, Tuple
8
+ from typing import Dict, Union, Optional, Tuple, List
9
9
 
10
10
  import matplotlib.pyplot as plt
11
11
  import numpy as np
@@ -19,19 +19,22 @@ import rangekeeper as rk
19
19
  def _format_series(
20
20
  series: pd.Series,
21
21
  units: pint.Unit,
22
+ to_datestamps: bool = True,
22
23
  decimals: int = 2,
23
24
  ):
25
+ index = pd.Series(series.index.date, name="date") if to_datestamps else series.index
26
+
24
27
  if units.dimensionality == "[currency]":
25
28
  formatted = pd.Series(
26
29
  data=[str(locale.currency(value, grouping=True)) for value in series],
27
- index=pd.Series(series.index, name="date"),
30
+ index=index,
28
31
  name=series.name,
29
32
  )
30
33
  else:
31
34
  floatfmt = "{:." + str(decimals) + "f}"
32
35
  formatted = pd.Series(
33
36
  data=[str(floatfmt.format(value)) for value in series],
34
- index=pd.Series(series.index, name="date"),
37
+ index=index,
35
38
  name=series.name,
36
39
  )
37
40
 
@@ -133,7 +136,9 @@ class Flow:
133
136
  "Movements: "
134
137
  + os.linesep
135
138
  + _format_series(
136
- series=self.movements, units=self.units, decimals=decimals
139
+ series=self.movements,
140
+ units=self.units,
141
+ decimals=decimals,
137
142
  ).to_markdown(
138
143
  stralign="right",
139
144
  numalign="right",
@@ -393,10 +398,10 @@ class Flow:
393
398
 
394
399
  class Stream:
395
400
  name: str
396
- flows: [Flow]
401
+ flows: List[Flow]
397
402
  frequency: rk.duration.Type
398
- start_date: pd.Timestamp
399
- end_date: pd.Timestamp
403
+ start_date: datetime.date
404
+ end_date: datetime.date
400
405
  frame: pd.DataFrame
401
406
 
402
407
  """
@@ -405,7 +410,7 @@ class Stream:
405
410
 
406
411
  def __init__(
407
412
  self,
408
- flows: [Flow],
413
+ flows: List[Flow],
409
414
  frequency: rk.duration.Type,
410
415
  name: str = None,
411
416
  ):
@@ -437,9 +442,9 @@ class Stream:
437
442
  self.start_date = min(flows_dates)
438
443
  self.end_date = max(flows_dates)
439
444
 
440
- index = rk.duration.Sequence.from_bounds(
441
- include_start=self.start_date, frequency=self.frequency, bound=self.end_date
442
- )
445
+ # index = rk.duration.Sequence.from_bounds(
446
+ # include_start=self.start_date, frequency=self.frequency, bound=self.end_date
447
+ # )
443
448
  self._resampled_flows = [
444
449
  flow.to_periods(frequency=self.frequency) for flow in self.flows
445
450
  ]
@@ -465,10 +470,22 @@ class Stream:
465
470
  for flow in self.flows:
466
471
  series = flow.to_periods(frequency=self.frequency)
467
472
  formatted_flows.append(
468
- _format_series(series=series, units=flow.units, decimals=decimals)
473
+ _format_series(
474
+ series=series,
475
+ units=flow.units,
476
+ to_datestamps=False,
477
+ decimals=decimals,
478
+ )
469
479
  )
470
480
  # formatted_flows = [flow._format_movements(decimals=decimals) for flow in self._resampled_flows]
471
- frame = pd.concat(formatted_flows, axis=1).fillna(0).sort_index()
481
+ frame = (
482
+ pd.concat(
483
+ formatted_flows,
484
+ axis=1,
485
+ )
486
+ .fillna(0)
487
+ .sort_index()
488
+ )
472
489
  return frame
473
490
 
474
491
  def display(
@@ -487,7 +504,10 @@ class Stream:
487
504
  "Flows: "
488
505
  + os.linesep
489
506
  + self._format_flows(decimals=decimals).to_markdown(
490
- tablefmt=tablefmt, stralign="right", numalign="right", floatfmt=floatfmt
507
+ tablefmt=tablefmt,
508
+ stralign="right",
509
+ numalign="right",
510
+ floatfmt=floatfmt,
491
511
  )
492
512
  )
493
513
 
@@ -10,48 +10,71 @@ import rangekeeper as rk
10
10
  class Balance:
11
11
  startings: rk.flux.Flow
12
12
  endings: rk.flux.Flow
13
+ overdraft: rk.flux.Flow
13
14
 
14
15
  def __init__(
15
- self,
16
- starting: float,
17
- transactions: rk.flux.Flow,
18
- frequency: rk.duration.Type,
16
+ self,
17
+ starting: float,
18
+ transactions: rk.flux.Flow,
19
+ frequency: rk.duration.Type,
20
+ name: str = "",
19
21
  ):
20
22
  """
21
23
  Calculate the balance of a financial account given a starting balance and a series of transactions.
24
+ Returns an object with the starting and ending balances, as well as the overdraft amount, as `Flow`s.
25
+ Overdraft is the portion of transactions occurring while the balance is non-positive.
22
26
  """
23
27
 
24
28
  transactions = transactions.resample(frequency=frequency)
25
29
 
26
30
  result = self._calculate_balance(
27
31
  startings=numba.typed.List([np.float64(starting)]),
28
- transactions=numba.typed.List(transactions.movements.to_list()))
32
+ transactions=numba.typed.List(transactions.movements.to_list()),
33
+ )
29
34
 
30
- self.startings=rk.flux.Flow(
31
- movements=pd.Series(result[0], index=transactions.movements.index),
35
+ self.startings = rk.flux.Flow(
36
+ movements=pd.Series(
37
+ result[0],
38
+ index=transactions.movements.index,
39
+ ),
32
40
  units=transactions.units,
33
- name='Start Balance')
41
+ name=f"{name} Start Balance",
42
+ )
34
43
 
35
- self.endings=rk.flux.Flow(
36
- movements=pd.Series(result[1], index=transactions.movements.index),
44
+ self.endings = rk.flux.Flow(
45
+ movements=pd.Series(
46
+ result[1],
47
+ index=transactions.movements.index,
48
+ ),
37
49
  units=transactions.units,
38
- name='End Balance')
50
+ name=f"{name} End Balance",
51
+ )
52
+
53
+ self.overdraft = rk.flux.Flow(
54
+ movements=pd.Series(
55
+ result[2],
56
+ index=transactions.movements.index,
57
+ ),
58
+ units=transactions.units,
59
+ name=f"{name} Overdraft",
60
+ )
39
61
 
40
62
  @classmethod
41
63
  def from_flows(
42
- cls,
43
- startings: rk.flux.Flow,
44
- endings: rk.flux.Flow,
64
+ cls,
65
+ startings: rk.flux.Flow,
66
+ endings: rk.flux.Flow,
45
67
  ):
46
68
  balance = super().__new__(cls)
47
69
  balance.startings = startings
48
70
  balance.endings = endings
71
+ balance.overdraft = balance._overdraft()
49
72
 
50
73
  return balance
51
74
 
52
- def overdraft(
53
- self,
54
- name: str = None,
75
+ def _overdraft(
76
+ self,
77
+ name: str = None,
55
78
  ) -> rk.flux.Flow:
56
79
  """
57
80
  Identifies the portion of transactions occurring while the balance is negative.
@@ -66,21 +89,35 @@ class Balance:
66
89
  return rk.flux.Flow(
67
90
  movements=overdrafts,
68
91
  units=self.startings.units,
69
- name='Overdraft' if name is None else name)
70
-
92
+ name="Overdraft" if name is None else name,
93
+ )
71
94
 
72
95
  @staticmethod
73
96
  @numba.jit
74
97
  def _calculate_balance(
75
- startings: numba.typed.List,
76
- transactions: numba.typed.List) -> (numba.typed.List, numba.typed.List):
98
+ startings: numba.typed.List,
99
+ transactions: numba.typed.List,
100
+ ) -> (
101
+ numba.typed.List,
102
+ numba.typed.List,
103
+ numba.typed.List,
104
+ ):
77
105
 
78
106
  endings = numba.typed.List.empty_list(numba.float64)
107
+ overdrafts = numba.typed.List.empty_list(numba.float64)
79
108
  for i in range(len(transactions)):
80
- endings.append(float(startings[-1] + transactions[i]))
109
+ ending = float(startings[-1] + transactions[i])
110
+
111
+ overdrafts.append(ending if ending < 0 else 0)
112
+ endings.append(ending if ending > 0 else 0)
81
113
  startings.append(endings[-1])
114
+ # startings.append(ending)
82
115
 
83
- return (startings[:-1], endings)
116
+ return (
117
+ startings[:-1],
118
+ endings,
119
+ overdrafts,
120
+ )
84
121
 
85
122
 
86
123
  class Interest:
@@ -88,16 +125,16 @@ class Interest:
88
125
  balance: Balance
89
126
 
90
127
  class Type(enum.Enum):
91
- INTEREST_ONLY = 'interest_only'
92
- COMPOUND = 'compound'
93
- CAPITALIZED = 'capitalized'
128
+ INTEREST_ONLY = "interest_only"
129
+ COMPOUND = "compound"
130
+ CAPITALIZED = "capitalized"
94
131
 
95
132
  def __init__(
96
- self,
97
- rate: float | rk.flux.Flow,
98
- transactions: rk.flux.Flow,
99
- frequency: rk.duration.Type,
100
- type: Type = Type.COMPOUND,
133
+ self,
134
+ rate: float | rk.flux.Flow,
135
+ transactions: rk.flux.Flow,
136
+ frequency: rk.duration.Type,
137
+ type: Type = Type.COMPOUND,
101
138
  ):
102
139
  """
103
140
  Calculate interest expense on a loan or other interest-bearing liability,
@@ -111,47 +148,54 @@ class Interest:
111
148
 
112
149
  if isinstance(rate, float):
113
150
  rate = rk.flux.Flow.from_projection(
114
- name='Interest Rates',
151
+ name="Interest Rates",
115
152
  value=rate,
116
153
  proj=rk.projection.Extrapolation(
117
154
  form=rk.extrapolation.Recurring(),
118
155
  sequence=rk.duration.Sequence.from_datestamps(
119
- datestamps=transactions.movements.index,
120
- frequency=frequency)))
156
+ datestamps=transactions.movements.index, frequency=frequency
157
+ ),
158
+ ),
159
+ )
121
160
  else:
122
- raise NotImplementedError('Only float rates are supported currently.')
161
+ raise NotImplementedError("Only float rates are supported currently.")
123
162
 
124
163
  if len(transactions.movements) != len(rate.movements):
125
- raise ValueError('The number of transactions must match the number of rates.')
164
+ raise ValueError(
165
+ "The number of transactions must match the number of rates."
166
+ )
126
167
 
127
168
  startings, endings, interests = self._calc_interest(
128
169
  transactions=numba.typed.List(transactions.movements.to_list()),
129
170
  rates=numba.typed.List(rate.movements.to_list()),
130
- type=type.value)
171
+ type=type.value,
172
+ )
131
173
 
132
174
  self.balance = rk.formula.financial.Balance.from_flows(
133
175
  startings=rk.flux.Flow(
134
176
  movements=pd.Series(startings, index=transactions.movements.index),
135
177
  units=transactions.units,
136
- name='Start Balance'),
178
+ name="Start Balance",
179
+ ),
137
180
  endings=rk.flux.Flow(
138
181
  movements=pd.Series(endings, index=transactions.movements.index),
139
182
  units=transactions.units,
140
- name='End Balance')
183
+ name="End Balance",
184
+ ),
141
185
  )
142
186
 
143
187
  self.amounts = rk.flux.Flow(
144
188
  movements=pd.Series(interests, index=transactions.movements.index),
145
189
  units=transactions.units,
146
- name='Interest')
147
-
190
+ name="Interest",
191
+ )
148
192
 
149
193
  @staticmethod
150
194
  @numba.jit
151
195
  def _calc_interest(
152
- transactions: numba.typed.List,
153
- rates: numba.typed.List,
154
- type: str,
196
+ transactions: numba.typed.List,
197
+ rates: numba.typed.List,
198
+ type: str,
155
199
  ) -> (numba.typed.List, numba.typed.List, numba.typed.List):
156
200
 
157
201
  startings = numba.typed.List.empty_list(numba.float64)
@@ -165,25 +209,24 @@ class Interest:
165
209
  startings.append(endings[-1])
166
210
 
167
211
  principal = startings[i] + transactions[i]
168
- if type == 'interest_only':
212
+ if type == "interest_only":
169
213
  interest = principal * rates[i] if principal > 0 else 0
170
214
  endings.append(principal)
171
215
 
172
- elif type == 'compound':
216
+ elif type == "compound":
173
217
  interest = principal * rates[i] if principal > 0 else 0
174
218
  endings.append(principal + interest)
175
219
 
176
- elif type == 'capitalized':
177
- interest = (principal * rates[i]) / (1 - rates[i]) if principal > 0 else 0 # Since we are capitalizing
220
+ elif type == "capitalized":
221
+ interest = (
222
+ (principal * rates[i]) / (1 - rates[i]) if principal > 0 else 0
223
+ ) # Since we are capitalizing
178
224
  # interest, the amount (draw) must include interest to pay on the principal. Derived from i = r * (P + i)
179
225
  endings.append(principal + interest)
180
226
 
181
227
  else:
182
- raise ValueError(f'Invalid instrument: {type}')
228
+ raise ValueError(f"Invalid instrument: {type}")
183
229
 
184
230
  amounts.append(interest)
185
231
 
186
232
  return (startings, endings, amounts)
187
-
188
-
189
-
@@ -1,90 +1,93 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
- import pprint as pp
5
3
  # Pytests file.
6
4
  # Note: gathers tests according to a naming convention.
7
5
  # By default any file that is to contain tests must be named starting with 'test_',
8
6
  # classes that hold tests must be named starting with 'Test',
9
7
  # and any function in a file that should be treated as a test must also start with 'test_'.
10
8
  import os
11
- import locale
12
- import multiprocess as mp
13
- from typing import List, Callable, Dict
14
-
15
- import matplotlib
16
- import matplotlib.pyplot as plt
17
- import networkx as nx
18
- from pyvis.network import Network
19
- import plotly.express as px
20
- import plotly.graph_objects as go
9
+ import pprint as pp
10
+
21
11
  # In addition, in order to enable pytest to find all modules,
22
12
  # run tests via a 'python -m pytest tests/<test_file>.py' command from the root directory of this project
23
13
  import pandas as pd
24
- pd.set_option('display.max_columns', None)
25
- import numpy as np
26
- import scipy.stats as ss
27
- import pint
14
+
15
+ pd.set_option("display.max_columns", None)
28
16
 
29
17
  import rangekeeper as rk
30
18
 
31
19
 
32
20
  class TestApi:
33
21
  stream_id = "c0f66c35e3"
34
- speckle = rk.api.Speckle(token=os.getenv('SPECKLE_TOKEN'))
22
+ speckle = rk.api.Speckle(token=os.getenv("SPECKLE_TOKEN"))
35
23
  model = speckle.get_commit(stream_id=stream_id)
36
24
 
37
25
  def test_connection(self):
38
- latest_commit_id = TestApi.speckle.get_latest_commit_id(stream_id=TestApi.stream_id)
39
- print('Latest Commit: {0}'.format(latest_commit_id))
26
+ latest_commit_id = TestApi.speckle.get_latest_commit_id(
27
+ stream_id=TestApi.stream_id
28
+ )
29
+ print("Latest Commit: {0}".format(latest_commit_id))
40
30
 
41
31
  metadata = TestApi.speckle.get_metadata(stream_id=TestApi.stream_id)
42
32
  print(metadata)
43
33
 
44
- assert metadata.name == 'RangekeeperDemo'
34
+ assert metadata.name == "RangekeeperDemo"
45
35
 
46
36
  roots = TestApi.model.get_dynamic_member_names()
47
37
 
48
38
  assert len(roots) == 2
49
- assert '@property' in roots
39
+ assert "@property" in roots
50
40
 
51
41
  def test_conversion(self):
52
- parsed = rk.api.Speckle.parse(base=TestApi.model['@property'])
42
+ parsed = rk.api.Speckle.parse(base=TestApi.model["@property"])
53
43
  root_base = list(parsed.values())[0]
54
- print('Root Base:\n {0}'.format(root_base))
55
- assert root_base.speckle_type == 'Rangekeeper.Entity:Rangekeeper.Assembly'
44
+ print("Root Base:\n {0}".format(root_base))
45
+ assert root_base.speckle_type == "Rangekeeper.Entity:Rangekeeper.Assembly"
56
46
 
57
47
  root_entity = rk.graph.Entity.from_base(root_base)
58
- assert root_entity.name == 'property'
48
+ assert root_entity.name == "property"
59
49
  assert len(root_entity.graph.nodes) == 5
60
50
  assert type(root_entity) == rk.graph.Assembly
61
- print('Root Entity:\n {0}'.format(root_entity))
62
-
63
- buildingA = [node for node in root_entity.graph.nodes if node.name == 'buildingA'][0]
64
- print('BuildingA:\n {0}'.format(buildingA))
65
- buildingA_containment = buildingA.get_relatives(outgoing=True, relationship_type='contains')
66
- print('BuildingA Containment: \n {0}\n'.format(buildingA_containment))
67
-
68
- buildingAresidential = [node for node in buildingA.graph.nodes if node.name == 'buildingAresidential'][0]
69
- print('BuildingAresidential:\n {0}'.format(buildingAresidential))
51
+ print("Root Entity:\n {0}".format(root_entity))
52
+
53
+ buildingA = [
54
+ node for node in root_entity.graph.nodes if node.name == "buildingA"
55
+ ][0]
56
+ print("BuildingA:\n {0}".format(buildingA))
57
+ buildingA_containment = buildingA.get_relatives(
58
+ outgoing=True, relationship_type="contains"
59
+ )
60
+ print("BuildingA Containment: \n {0}\n".format(buildingA_containment))
61
+
62
+ buildingAresidential = [
63
+ node
64
+ for node in buildingA.graph.nodes
65
+ if node.name == "buildingAresidential"
66
+ ][0]
67
+ print("BuildingAresidential:\n {0}".format(buildingAresidential))
70
68
 
71
69
  def test_develop(self):
72
- parsed = rk.api.Speckle.parse(base=TestApi.model['@property'])
70
+ parsed = rk.api.Speckle.parse(base=TestApi.model["@property"])
73
71
  # print('\nParsed: \n{0}'.format(pp.pprint([base['name'] for base in parsed.values()])))
74
72
  # print('\nCount Parsed: \n{0}'.format(len(parsed)))
75
73
 
76
74
  property = rk.api.Speckle.to_rk(
77
- bases=list(parsed.values()),
78
- name='property',
79
- type='property')
75
+ bases=list(parsed.values()), name="property", type="property"
76
+ )
80
77
  # print('\nScenario: \n{0}'.format(scenario))
81
78
  # print('\nCount Scenario: \n{0}'.format(len(scenario.graph.nodes())))
82
79
 
83
80
  spatial_containment = rk.graph.Assembly.from_graph(
84
81
  graph=property.graph.edge_subgraph(
85
- [edge for edge in property.graph.edges(keys=True) if edge[2] == 'spatiallyContains']),
86
- name='spatial_containment',
87
- type='subgraph')
82
+ [
83
+ edge
84
+ for edge in property.graph.edges(keys=True)
85
+ if edge[2] == "spatiallyContains"
86
+ ]
87
+ ),
88
+ name="spatial_containment",
89
+ type="subgraph",
90
+ )
88
91
  # print('\nSpatial Containment: \n{0}'.format(spatial_containment))
89
92
 
90
93
  roots = property.get_roots() # ['spatiallyContains']
@@ -93,7 +96,11 @@ class TestApi:
93
96
  subassemblies = property.get_subassemblies()
94
97
  # print('\nSubassemblies: \n{0}'.format(pp.pprint(list(subassemblies.values()))))
95
98
 
96
- buildingA = [assembly for assembly in subassemblies.values() if assembly['name'] == 'buildingA'][0]
99
+ buildingA = [
100
+ assembly
101
+ for assembly in subassemblies.values()
102
+ if assembly["name"] == "buildingA"
103
+ ][0]
97
104
  # print('\nBuildingA: \n{0}'.format(buildingA))
98
105
 
99
106
  subentities = buildingA.get_subentities()
@@ -106,27 +113,26 @@ class TestApi:
106
113
  hierarchical_layout=False,
107
114
  display=False,
108
115
  height=1600,
109
- )
116
+ )
110
117
 
111
118
  spatial_containment_dict = spatial_containment.to_dict()
112
119
  # print('\nSpatial Containment Dict: \n{0}'.format(pp.pprint(spatial_containment_dict)))
113
120
 
114
- spatial_containment.aggregate(
115
- property='gfa',
116
- label='subtotal_gfa')
121
+ spatial_containment.aggregate(property="gfa", label="subtotal_gfa")
117
122
 
118
- print('\nGFA Aggregation: \n{0}'.format(pp.pprint(spatial_containment.to_dict())))
123
+ print(
124
+ "\nGFA Aggregation: \n{0}".format(pp.pprint(spatial_containment.to_dict()))
125
+ )
119
126
  #
120
- df = pd.DataFrame.from_dict(spatial_containment.to_dict(), orient='index')
127
+ df = pd.DataFrame.from_dict(spatial_containment.to_dict(), orient="index")
121
128
  print(df)
122
129
 
123
130
  foo = spatial_containment.to_DataFrame()
124
131
  #
125
132
 
126
- fig = spatial_containment.sunburst('subtotal_gfa')
133
+ fig = spatial_containment.sunburst("subtotal_gfa")
127
134
  fig.show()
128
135
 
129
-
130
136
  #
131
137
 
132
138
  #
@@ -122,7 +122,9 @@ class TestFinancial:
122
122
  draws_periods=9,
123
123
  payments=value,
124
124
  payments_start_date=rk.duration.offset(
125
- date=start_date, duration=rk.duration.Type.MONTH, amount=9
125
+ date=start_date,
126
+ duration=rk.duration.Type.MONTH,
127
+ amount=9,
126
128
  ),
127
129
  payments_periods=3,
128
130
  )
@@ -206,9 +208,7 @@ class TestFinancial:
206
208
  def test_balances(self):
207
209
  transactions = rk.flux.Stream(
208
210
  name="Transactions",
209
- flows=[
210
- self.model.draws.sum().invert(),
211
- ],
211
+ flows=[self.model.draws.sum().invert()],
212
212
  frequency=self.params["frequency"],
213
213
  )
214
214
  equity = rk.formula.financial.Balance(
@@ -221,14 +221,17 @@ class TestFinancial:
221
221
  equity.startings.display()
222
222
  equity.endings.display()
223
223
 
224
- overdraft = equity.overdraft()
224
+ overdraft = equity.overdraft
225
225
  overdraft.display()
226
226
 
227
227
  interest = rk.formula.financial.Interest(
228
228
  rate=self.params["interest_rate_pa"]
229
229
  / rk.duration.Period.yearly_count((self.params["frequency"])),
230
230
  transactions=rk.flux.Stream(
231
- flows=[overdraft.invert(), self.model.payments.invert()],
231
+ flows=[
232
+ overdraft.invert(),
233
+ self.model.payments.invert(),
234
+ ],
232
235
  frequency=self.params["frequency"],
233
236
  ).sum(),
234
237
  frequency=self.params["frequency"],
@@ -239,7 +242,8 @@ class TestFinancial:
239
242
  interest.amounts.display()
240
243
  print(interest.amounts.total())
241
244
 
242
- profit = interest.balance.overdraft(name="Profit").invert()
245
+ profit = interest.balance.overdraft.invert()
246
+ profit.name = "Profit"
243
247
  profit.display()
244
248
 
245
249
  assert profit.total() == approx(671969.16)
@@ -1,18 +0,0 @@
1
- # Rangekeeper Source
2
- This directory holds the source code for the Rangekeeper Library
3
-
4
- ## Installation
5
- The library is offered through PyPI, and so its installation in other projects can be performed by:
6
- `pip install rangekeeper` or `poetry add rangekeeper`
7
-
8
- ## Development
9
- If you wish to contribute to its development, it is recommended to use [Poetry](https://python-poetry.org/) for environment and dependency management:
10
-
11
- ### Environment Setup
12
-
13
- 1. Install poetry, if you haven't yet: <https://python-poetry.org/docs/master/#installing-with-the-official-installer>
14
- 2. Clone this repo.
15
- 3. Use a terminal to install poetry packages from the repo's directory: `<path_to_repo>$ poetry install`
16
- 4. If you wish to develop this repo alongside other projects locally, you may install the local Rangekeeper library via `<path_to_repo>$ poetry run pip install -e <path_to_rangekeeper_repo>`,
17
- replacing `<path_to_rangekeeper_repo>` with its location on your system.
18
- 5. Some tests require API access to [Speckle](https://speckle.systems/). It is recommended to use the [Poetry Dotenv Plugin](https://github.com/mpeteuil/poetry-dotenv-plugin) via `poetry self add poetry-dotenv-plugin`, and add a `.env` file in the project's root directory with your `SPECKLE_TOKEN` environment variable.
File without changes