pyproforma 0.2.2__tar.gz → 0.2.4__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 (69) hide show
  1. {pyproforma-0.2.2/pyproforma.egg-info → pyproforma-0.2.4}/PKG-INFO +12 -10
  2. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/__init__.py +26 -21
  3. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/chart/renderers/matplotlib_renderer.py +2 -2
  4. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/charts/chart_def.py +1 -2
  5. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/charts/charts.py +4 -1
  6. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/compare/model_comparison.py +3 -1
  7. pyproforma-0.2.4/pyproforma/engine/__init__.py +14 -0
  8. {pyproforma-0.2.2/pyproforma → pyproforma-0.2.4/pyproforma/engine}/calculation_engine.py +23 -15
  9. {pyproforma-0.2.2/pyproforma/line_items → pyproforma-0.2.4/pyproforma/engine}/line_item_values.py +44 -2
  10. {pyproforma-0.2.2/pyproforma → pyproforma-0.2.4/pyproforma/engine}/model_namespace.py +22 -2
  11. pyproforma-0.2.4/pyproforma/explorer/__init__.py +4 -0
  12. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/explorer/app.py +117 -45
  13. pyproforma-0.2.4/pyproforma/explorer/components.py +128 -0
  14. pyproforma-0.2.4/pyproforma/explorer/templates/base.html +138 -0
  15. pyproforma-0.2.4/pyproforma/explorer/templates/chart_view.html +28 -0
  16. pyproforma-0.2.4/pyproforma/explorer/templates/index.html +80 -0
  17. pyproforma-0.2.4/pyproforma/explorer/templates/inputs.html +107 -0
  18. pyproforma-0.2.4/pyproforma/explorer/templates/line_item.html +173 -0
  19. pyproforma-0.2.4/pyproforma/explorer/templates/table_view.html +18 -0
  20. pyproforma-0.2.4/pyproforma/explorer/templates/view.html +178 -0
  21. pyproforma-0.2.4/pyproforma/proforma_model.py +248 -0
  22. pyproforma-0.2.4/pyproforma/results/__init__.py +13 -0
  23. {pyproforma-0.2.2/pyproforma/line_items → pyproforma-0.2.4/pyproforma/results}/line_item_result.py +26 -82
  24. {pyproforma-0.2.2/pyproforma/line_items → pyproforma-0.2.4/pyproforma/results}/line_item_selection.py +1 -1
  25. pyproforma-0.2.4/pyproforma/results/line_item_stat.py +120 -0
  26. pyproforma-0.2.4/pyproforma/results/scalar_result.py +41 -0
  27. pyproforma-0.2.4/pyproforma/results/tags_namespace.py +34 -0
  28. pyproforma-0.2.4/pyproforma/specs/__init__.py +33 -0
  29. {pyproforma-0.2.2/pyproforma/line_items → pyproforma-0.2.4/pyproforma/specs}/debt_line.py +59 -52
  30. pyproforma-0.2.4/pyproforma/specs/fixed_line.py +35 -0
  31. {pyproforma-0.2.2/pyproforma/line_items → pyproforma-0.2.4/pyproforma/specs}/formula_line.py +1 -1
  32. {pyproforma-0.2.2/pyproforma/line_items → pyproforma-0.2.4/pyproforma/specs}/input_line.py +47 -12
  33. {pyproforma-0.2.2/pyproforma/line_items → pyproforma-0.2.4/pyproforma/specs}/line_item.py +25 -9
  34. pyproforma-0.2.4/pyproforma/specs/scalar_input_line.py +54 -0
  35. pyproforma-0.2.4/pyproforma/specs/scalar_line.py +38 -0
  36. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/table/colors.py +25 -26
  37. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/table/excel.py +14 -7
  38. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/table/format_value.py +6 -2
  39. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/table/table_class.py +15 -5
  40. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/tables/row_types.py +15 -8
  41. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/tables/table_def.py +1 -2
  42. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/tables/tables.py +5 -2
  43. {pyproforma-0.2.2 → pyproforma-0.2.4/pyproforma.egg-info}/PKG-INFO +12 -10
  44. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma.egg-info/SOURCES.txt +25 -12
  45. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma.egg-info/requires.txt +11 -6
  46. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproject.toml +26 -14
  47. pyproforma-0.2.2/pyproforma/explorer/__init__.py +0 -3
  48. pyproforma-0.2.2/pyproforma/explorer/components.py +0 -42
  49. pyproforma-0.2.2/pyproforma/line_items/__init__.py +0 -38
  50. pyproforma-0.2.2/pyproforma/line_items/fixed_line.py +0 -72
  51. pyproforma-0.2.2/pyproforma/proforma_model.py +0 -191
  52. pyproforma-0.2.2/pyproforma/tags_namespace.py +0 -220
  53. {pyproforma-0.2.2 → pyproforma-0.2.4}/LICENSE +0 -0
  54. {pyproforma-0.2.2 → pyproforma-0.2.4}/MANIFEST.in +0 -0
  55. {pyproforma-0.2.2 → pyproforma-0.2.4}/README.md +0 -0
  56. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/chart/__init__.py +0 -0
  57. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/chart/chart.py +0 -0
  58. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/chart/renderers/__init__.py +0 -0
  59. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/chart/renderers/base.py +0 -0
  60. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/charts/__init__.py +0 -0
  61. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/compare/__init__.py +0 -0
  62. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/reserved_words.py +0 -0
  63. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/table/__init__.py +0 -0
  64. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/table/bootstrap_html_renderer.py +0 -0
  65. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/table/html_renderer.py +0 -0
  66. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma/tables/__init__.py +0 -0
  67. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma.egg-info/dependency_links.txt +0 -0
  68. {pyproforma-0.2.2 → pyproforma-0.2.4}/pyproforma.egg-info/top_level.txt +0 -0
  69. {pyproforma-0.2.2 → pyproforma-0.2.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyproforma
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: A Python package for financial modeling and reporting
5
5
  Author-email: Robert Hannay <rhannay@gmail.com>
6
6
  Maintainer-email: Robert Hannay <rhannay@gmail.com>
@@ -22,19 +22,21 @@ Classifier: Operating System :: OS Independent
22
22
  Requires-Python: >=3.9
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- Requires-Dist: pandas>=1.3.0
26
- Requires-Dist: openpyxl>=3.0.0
27
- Requires-Dist: jinja2>=3.0.0
28
- Requires-Dist: plotly>=5.0.0
29
- Requires-Dist: PyYAML>=6.0.0
30
- Requires-Dist: nbformat>=5.0.0
31
- Provides-Extra: dev
32
- Requires-Dist: pytest>=6.0; extra == "dev"
33
- Requires-Dist: pytest-cov; extra == "dev"
25
+ Provides-Extra: pandas
26
+ Requires-Dist: pandas>=1.3.0; extra == "pandas"
27
+ Provides-Extra: excel
28
+ Requires-Dist: openpyxl>=3.0.0; extra == "excel"
29
+ Provides-Extra: charts
30
+ Requires-Dist: matplotlib>=3.5.0; extra == "charts"
34
31
  Provides-Extra: notebook
35
32
  Requires-Dist: ipython>=7.0.0; extra == "notebook"
36
33
  Provides-Extra: explorer
37
34
  Requires-Dist: flask>=2.0.0; extra == "explorer"
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=6.0; extra == "dev"
37
+ Requires-Dist: pytest-cov; extra == "dev"
38
+ Requires-Dist: pandas>=1.3.0; extra == "dev"
39
+ Requires-Dist: openpyxl>=3.0.0; extra == "dev"
38
40
  Dynamic: license-file
39
41
 
40
42
  # pyproforma
@@ -3,7 +3,26 @@ PyProforma - A lightweight financial modeling framework.
3
3
  """
4
4
 
5
5
  from .charts.chart_def import ChartDef
6
- from .tables.table_def import TableDef
6
+ from .compare import ModelComparison
7
+ from .engine import LineItemValue, LineItemValues
8
+ from .proforma_model import ProformaModel
9
+ from .results import LineItemResult, LineItemSelection, ScalarResult
10
+ from .results.tags_namespace import TagNamespace
11
+ from .specs import (
12
+ DebtCalculator,
13
+ DebtConfig,
14
+ DebtInterestLine,
15
+ DebtPrincipalLine,
16
+ FixedLine,
17
+ FormulaLine,
18
+ InputLine,
19
+ LineItem,
20
+ ScalarInputLine,
21
+ ScalarLine,
22
+ create_debt_lines,
23
+ )
24
+ from .table import Format, NumberFormatSpec
25
+ from .tables import Tables
7
26
  from .tables.row_types import (
8
27
  BlankRow,
9
28
  CumulativeChangeRow,
@@ -16,25 +35,7 @@ from .tables.row_types import (
16
35
  TagItemsRow,
17
36
  TagTotalRow,
18
37
  )
19
- from .line_items import (
20
- DebtCalculator,
21
- DebtInterestLine,
22
- DebtPrincipalLine,
23
- FixedLine,
24
- FormulaLine,
25
- InputLine,
26
- LineItem,
27
- LineItemResult,
28
- LineItemSelection,
29
- LineItemValue,
30
- LineItemValues,
31
- create_debt_lines,
32
- )
33
- from .table import Format, NumberFormatSpec
34
- from .compare import ModelComparison
35
- from .proforma_model import ProformaModel
36
- from .tables import Tables
37
- from .tags_namespace import ModelTagNamespace
38
+ from .tables.table_def import TableDef
38
39
 
39
40
  __all__ = [
40
41
  "ProformaModel",
@@ -53,16 +54,20 @@ __all__ = [
53
54
  "FixedLine",
54
55
  "FormulaLine",
55
56
  "InputLine",
57
+ "ScalarLine",
58
+ "ScalarInputLine",
59
+ "ScalarResult",
56
60
  "DebtPrincipalLine",
57
61
  "DebtInterestLine",
58
62
  "DebtCalculator",
63
+ "DebtConfig",
59
64
  "create_debt_lines",
60
65
  "LineItem",
61
66
  "LineItemValues",
62
67
  "LineItemValue",
63
68
  "LineItemResult",
64
69
  "LineItemSelection",
65
- "ModelTagNamespace",
70
+ "TagNamespace",
66
71
  "Tables",
67
72
  "ModelComparison",
68
73
  "Format",
@@ -7,8 +7,8 @@ chart types. Applies NumberFormatSpec to y-axis tick labels when set.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from pyproforma.chart.renderers.base import ChartRenderer
11
10
  from pyproforma.chart.chart import Chart as ChartSpec
11
+ from pyproforma.chart.renderers.base import ChartRenderer
12
12
 
13
13
 
14
14
  class MatplotlibRenderer(ChartRenderer):
@@ -26,7 +26,6 @@ class MatplotlibRenderer(ChartRenderer):
26
26
  matplotlib.figure.Figure
27
27
  """
28
28
  import matplotlib.pyplot as plt
29
- import numpy as np
30
29
 
31
30
  fig, ax = plt.subplots(figsize=figsize)
32
31
 
@@ -109,6 +108,7 @@ class MatplotlibRenderer(ChartRenderer):
109
108
  return
110
109
 
111
110
  from matplotlib.ticker import FuncFormatter
111
+
112
112
  from pyproforma.table.format_value import format_value
113
113
 
114
114
  fmt = spec.value_format
@@ -6,8 +6,7 @@ chart type, title, and optional per-series colors. Accepts either the dataclass
6
6
  form (IDE autocomplete, validation) or a plain dict (JSON-serializable configs).
7
7
  """
8
8
 
9
- from dataclasses import dataclass, field
10
- from typing import Union
9
+ from dataclasses import dataclass
11
10
 
12
11
  from pyproforma.chart.chart import ChartType
13
12
 
@@ -15,6 +15,7 @@ from typing import TYPE_CHECKING
15
15
  from pyproforma.chart.chart import Chart, ChartSeries, ChartType
16
16
 
17
17
  if TYPE_CHECKING:
18
+ from pyproforma.charts.chart_def import ChartDef
18
19
  from pyproforma.proforma_model import ProformaModel
19
20
 
20
21
 
@@ -113,7 +114,9 @@ class Charts:
113
114
  Examples:
114
115
  >>> model.charts.line_items(["revenue", "expenses"]).show()
115
116
  >>> model.charts.line_items(["revenue", "cogs"], chart_type="stacked_bar").show()
116
- >>> model.charts.line_items(["revenue", "expenses"], value_format=Format.MILLIONS_M).show()
117
+ >>> model.charts.line_items( # noqa: E501
118
+ ... ["revenue", "expenses"], value_format=Format.MILLIONS_M
119
+ ... ).show()
117
120
  """
118
121
  for name in names:
119
122
  self._validate_line_item(name)
@@ -293,7 +293,9 @@ class ModelComparison:
293
293
  for model, label in zip(self.models, self.labels):
294
294
  row = [Cell(value=label, align="left")]
295
295
  for period in self.common_periods:
296
- row.append(Cell(value=model.get_value(item_name, period), value_format=value_format))
296
+ row.append(
297
+ Cell(value=model.get_value(item_name, period), value_format=value_format)
298
+ )
297
299
  all_rows.append(row)
298
300
 
299
301
  # Difference row(s)
@@ -0,0 +1,14 @@
1
+ """
2
+ Calculation internals — engine and namespace.
3
+ """
4
+
5
+ from .calculation_engine import calculate_line_items
6
+ from .line_item_values import LineItemValue, LineItemValues
7
+ from .model_namespace import ModelNamespace
8
+
9
+ __all__ = [
10
+ "calculate_line_items",
11
+ "LineItemValues",
12
+ "LineItemValue",
13
+ "ModelNamespace",
14
+ ]
@@ -8,7 +8,7 @@ values across periods.
8
8
  from typing import TYPE_CHECKING, Any
9
9
 
10
10
  if TYPE_CHECKING:
11
- from .line_items.line_item_values import LineItemValues
11
+ from .line_item_values import LineItemValues
12
12
 
13
13
 
14
14
  def calculate_line_items(
@@ -27,13 +27,15 @@ def calculate_line_items(
27
27
  Returns:
28
28
  LineItemValues: Populated container with all calculated values.
29
29
  """
30
- from .line_items.debt_line import DebtBase
31
- from .line_items.fixed_line import FixedLine
32
- from .line_items.formula_line import FormulaLine
33
- from .line_items.input_line import InputLine
34
- from .line_items.line_item_values import LineItemValues
30
+ from pyproforma.specs.debt_line import DebtBase
31
+ from pyproforma.specs.fixed_line import FixedLine
32
+ from pyproforma.specs.formula_line import FormulaLine
33
+ from pyproforma.specs.input_line import InputLine
34
+
35
+ from .line_item_values import LineItemValues
35
36
  from .model_namespace import ModelNamespace
36
37
 
38
+ # scalar_names are already resolved into the scalars dict — skip them here
37
39
  li = LineItemValues(periods=periods, names=model.line_item_names, model=model)
38
40
 
39
41
  fixed_items = []
@@ -80,7 +82,7 @@ def calculate_line_items(
80
82
  still_pending.append(name)
81
83
  else:
82
84
  raise ValueError(
83
- f"Error evaluating formula for '{line_item.name}' in period {period}: {e}"
85
+ f"Error evaluating formula for '{line_item.name}' in period {period}: {e}" # noqa: E501
84
86
  ) from e
85
87
 
86
88
  if len(still_pending) == len(remaining):
@@ -100,20 +102,19 @@ def _calculate_single_line_item(
100
102
  period: int,
101
103
  model: Any = None,
102
104
  ) -> Any:
103
- from .line_items.debt_line import DebtBase
104
- from .line_items.fixed_line import FixedLine
105
- from .line_items.formula_line import FormulaLine
106
- from .line_items.input_line import InputLine
105
+ from pyproforma.specs.debt_line import DebtBase
106
+ from pyproforma.specs.fixed_line import FixedLine
107
+ from pyproforma.specs.formula_line import FormulaLine
108
+ from pyproforma.specs.input_line import InputLine
107
109
 
108
110
  if isinstance(line_item, InputLine):
109
111
  input_values = getattr(model, "_input_line_values", {})
110
112
  period_values = input_values.get(line_item.name, {})
111
- value = period_values.get(period)
112
- if value is None:
113
+ if period not in period_values:
113
114
  raise ValueError(
114
115
  f"No input value for '{line_item.name}' in period {period}"
115
116
  )
116
- return value
117
+ return period_values[period] # None is a valid value — means "no input this period"
117
118
 
118
119
  if isinstance(line_item, FixedLine):
119
120
  value = line_item.get_value(period)
@@ -141,8 +142,15 @@ def _calculate_single_line_item(
141
142
  return value
142
143
 
143
144
  if isinstance(line_item, DebtBase):
145
+ debt_calculators = getattr(model, "_debt_calculators", {})
146
+ calculator = debt_calculators.get(id(line_item.config))
147
+ if calculator is None:
148
+ raise ValueError(
149
+ f"No calculator found for debt line '{line_item.name}'. "
150
+ f"Ensure the model was instantiated via ProformaModel.__init__."
151
+ )
144
152
  try:
145
- value = line_item.eval(ns, period)
153
+ value = line_item.eval(ns, period, calculator)
146
154
  except (AttributeError, KeyError):
147
155
  raise
148
156
  except Exception as e:
@@ -9,7 +9,49 @@ from typing import TYPE_CHECKING, Any
9
9
  if TYPE_CHECKING:
10
10
  from pyproforma.proforma_model import ProformaModel
11
11
 
12
- from pyproforma.tags_namespace import TagNamespace
12
+
13
+ class TagSum:
14
+ """Sums all line items sharing a tag for a given period. Used inside formula evaluation."""
15
+
16
+ def __init__(self, model: "ProformaModel", li: "LineItemValues", tag: str):
17
+ self._model = model
18
+ self._li = li
19
+ self._tag = tag
20
+
21
+ def __getitem__(self, period: int) -> float:
22
+ if period not in self._model.periods:
23
+ raise KeyError(
24
+ f"Period {period} not found in model. "
25
+ f"Available periods: {self._model.periods}"
26
+ )
27
+ total = 0.0
28
+ for name in self._model.line_item_names:
29
+ spec = getattr(self._model.__class__, name)
30
+ if hasattr(spec, "tags") and self._tag in spec.tags:
31
+ try:
32
+ value = self._li.get(name, period)
33
+ if value is not None:
34
+ total += value
35
+ except (AttributeError, KeyError):
36
+ pass
37
+ return total
38
+
39
+ def __repr__(self):
40
+ return f"TagSum(tag={self._tag!r})"
41
+
42
+
43
+ class TagNamespace:
44
+ """Namespace for tag-based summation inside formula evaluation (li.tag["revenue"][t])."""
45
+
46
+ def __init__(self, model: "ProformaModel", li: "LineItemValues"):
47
+ self._model = model
48
+ self._li = li
49
+
50
+ def __getitem__(self, tag: str) -> TagSum:
51
+ return TagSum(self._model, self._li, tag)
52
+
53
+ def __repr__(self):
54
+ return f"TagNamespace(model={self._model.__class__.__name__})"
13
55
 
14
56
 
15
57
  class LineItemValues:
@@ -77,7 +119,7 @@ class LineItemValues:
77
119
  self._tag_namespace = TagNamespace(model, self) if model else None
78
120
 
79
121
  @property
80
- def tag(self) -> TagNamespace:
122
+ def tag(self) -> "TagNamespace":
81
123
  """
82
124
  Access line items by tags.
83
125
 
@@ -12,7 +12,24 @@ Scalar line items (FixedLine with value=, or scalar InputLine) are accessed dire
12
12
  from typing import TYPE_CHECKING
13
13
 
14
14
  if TYPE_CHECKING:
15
- from pyproforma.line_items.line_item_values import LineItemValues
15
+ from pyproforma.engine.line_item_values import LineItemValues
16
+
17
+
18
+ class _ScalarValue(float):
19
+ """float subclass returned for scalar line items in formulas.
20
+
21
+ Raises a clear TypeError if the caller tries li.rate[t] instead of li.rate.
22
+ """
23
+
24
+ def __new__(cls, value: float, name: str):
25
+ inst = super().__new__(cls, value)
26
+ inst._name = name
27
+ return inst
28
+
29
+ def __getitem__(self, key):
30
+ raise TypeError(
31
+ f"'{self._name}' is a scalar — use li.{self._name}, not li.{self._name}[{key}]"
32
+ )
16
33
 
17
34
 
18
35
  class ModelNamespace:
@@ -48,7 +65,10 @@ class ModelNamespace:
48
65
  return li.tag
49
66
 
50
67
  if name in scalars:
51
- return scalars[name]
68
+ value = scalars[name]
69
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
70
+ return _ScalarValue(value, name)
71
+ return value
52
72
 
53
73
  li_error = None
54
74
  try:
@@ -0,0 +1,4 @@
1
+ from .app import create_app
2
+ from .components import InputGroup, StatCard
3
+
4
+ __all__ = ["create_app", "InputGroup", "StatCard"]