pyproforma 0.2.4__tar.gz → 0.3.0__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 (78) hide show
  1. pyproforma-0.3.0/PKG-INFO +379 -0
  2. pyproforma-0.3.0/README.md +337 -0
  3. pyproforma-0.3.0/pyproforma/cli.py +103 -0
  4. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/compare/model_comparison.py +90 -14
  5. pyproforma-0.3.0/pyproforma/explorer/__init__.py +5 -0
  6. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/explorer/app.py +224 -174
  7. pyproforma-0.3.0/pyproforma/explorer/compare_defs.py +89 -0
  8. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/explorer/components.py +9 -2
  9. pyproforma-0.3.0/pyproforma/explorer/config.py +195 -0
  10. pyproforma-0.3.0/pyproforma/explorer/scenario_app.py +384 -0
  11. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/explorer/templates/base.html +32 -9
  12. pyproforma-0.3.0/pyproforma/explorer/templates/compare_item.html +35 -0
  13. pyproforma-0.3.0/pyproforma/explorer/templates/compare_items.html +20 -0
  14. pyproforma-0.3.0/pyproforma/explorer/templates/compare_overview.html +21 -0
  15. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/explorer/templates/index.html +3 -3
  16. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/explorer/templates/line_item.html +6 -6
  17. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/explorer/templates/table_view.html +10 -0
  18. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/explorer/templates/view.html +13 -0
  19. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/proforma_model.py +22 -11
  20. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/reserved_words.py +0 -1
  21. pyproforma-0.3.0/pyproforma/results/line_item_stat.py +133 -0
  22. pyproforma-0.3.0/pyproforma/table/col_widths.py +17 -0
  23. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/table/excel.py +50 -54
  24. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/table/table_class.py +5 -0
  25. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/tables/row_types.py +9 -2
  26. pyproforma-0.3.0/pyproforma/tables/table_def.py +65 -0
  27. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/tables/tables.py +28 -8
  28. pyproforma-0.3.0/pyproforma.egg-info/PKG-INFO +379 -0
  29. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma.egg-info/SOURCES.txt +9 -1
  30. pyproforma-0.3.0/pyproforma.egg-info/entry_points.txt +2 -0
  31. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma.egg-info/requires.txt +1 -0
  32. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproject.toml +6 -2
  33. pyproforma-0.2.4/PKG-INFO +0 -244
  34. pyproforma-0.2.4/README.md +0 -203
  35. pyproforma-0.2.4/pyproforma/explorer/__init__.py +0 -4
  36. pyproforma-0.2.4/pyproforma/explorer/templates/inputs.html +0 -107
  37. pyproforma-0.2.4/pyproforma/results/line_item_stat.py +0 -120
  38. pyproforma-0.2.4/pyproforma/tables/table_def.py +0 -35
  39. pyproforma-0.2.4/pyproforma.egg-info/PKG-INFO +0 -244
  40. {pyproforma-0.2.4 → pyproforma-0.3.0}/LICENSE +0 -0
  41. {pyproforma-0.2.4 → pyproforma-0.3.0}/MANIFEST.in +0 -0
  42. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/__init__.py +0 -0
  43. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/chart/__init__.py +0 -0
  44. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/chart/chart.py +0 -0
  45. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/chart/renderers/__init__.py +0 -0
  46. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/chart/renderers/base.py +0 -0
  47. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/chart/renderers/matplotlib_renderer.py +0 -0
  48. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/charts/__init__.py +0 -0
  49. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/charts/chart_def.py +0 -0
  50. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/charts/charts.py +0 -0
  51. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/compare/__init__.py +0 -0
  52. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/engine/__init__.py +0 -0
  53. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/engine/calculation_engine.py +0 -0
  54. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/engine/line_item_values.py +0 -0
  55. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/engine/model_namespace.py +0 -0
  56. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/explorer/templates/chart_view.html +0 -0
  57. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/results/__init__.py +0 -0
  58. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/results/line_item_result.py +0 -0
  59. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/results/line_item_selection.py +0 -0
  60. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/results/scalar_result.py +0 -0
  61. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/results/tags_namespace.py +0 -0
  62. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/specs/__init__.py +0 -0
  63. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/specs/debt_line.py +0 -0
  64. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/specs/fixed_line.py +0 -0
  65. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/specs/formula_line.py +0 -0
  66. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/specs/input_line.py +0 -0
  67. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/specs/line_item.py +0 -0
  68. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/specs/scalar_input_line.py +0 -0
  69. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/specs/scalar_line.py +0 -0
  70. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/table/__init__.py +0 -0
  71. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/table/bootstrap_html_renderer.py +0 -0
  72. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/table/colors.py +0 -0
  73. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/table/format_value.py +0 -0
  74. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/table/html_renderer.py +0 -0
  75. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma/tables/__init__.py +0 -0
  76. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma.egg-info/dependency_links.txt +0 -0
  77. {pyproforma-0.2.4 → pyproforma-0.3.0}/pyproforma.egg-info/top_level.txt +0 -0
  78. {pyproforma-0.2.4 → pyproforma-0.3.0}/setup.cfg +0 -0
@@ -0,0 +1,379 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyproforma
3
+ Version: 0.3.0
4
+ Summary: A Python package for financial modeling and reporting
5
+ Author-email: Robert Hannay <rhannay@gmail.com>
6
+ Maintainer-email: Robert Hannay <rhannay@gmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/rhannay/pyproforma
9
+ Project-URL: Repository, https://github.com/rhannay/pyproforma
10
+ Project-URL: Documentation, https://pyproforma.readthedocs.io/en/latest/
11
+ Project-URL: Issues, https://github.com/rhannay/pyproforma/issues
12
+ Keywords: finance,modeling,reporting,excel,proforma
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Financial and Insurance Industry
15
+ Classifier: Topic :: Office/Business :: Financial
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Operating System :: OS Independent
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
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"
31
+ Provides-Extra: notebook
32
+ Requires-Dist: ipython>=7.0.0; extra == "notebook"
33
+ Provides-Extra: explorer
34
+ Requires-Dist: flask>=2.0.0; extra == "explorer"
35
+ Requires-Dist: PyYAML>=6.0.0; extra == "explorer"
36
+ Provides-Extra: dev
37
+ Requires-Dist: pytest>=6.0; extra == "dev"
38
+ Requires-Dist: pytest-cov; extra == "dev"
39
+ Requires-Dist: pandas>=1.3.0; extra == "dev"
40
+ Requires-Dist: openpyxl>=3.0.0; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ # pyproforma
44
+
45
+ A Python library for building financial models — a code-first alternative to Excel for pro formas, projections, and structured financial tables.
46
+
47
+ ```bash
48
+ pip install pyproforma
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Why
54
+
55
+ Spreadsheets are the default tool for financial modeling, but they have real problems: no version control, no testing, formulas hidden inside cells, and no easy way to generate the same model for multiple scenarios. pyproforma is designed for analysts who want the benefits of code — reproducibility, testability, version history — without giving up the tabular output that finance people actually use.
56
+
57
+ ---
58
+
59
+ ## How it works
60
+
61
+ Define a model by subclassing `ProformaModel` and declaring line items as class attributes. Instantiate it with a list of periods and the library calculates everything.
62
+
63
+ ```python
64
+ from pyproforma import ProformaModel, FixedLine, FormulaLine, ScalarLine, Format
65
+
66
+ class IncomeStatement(ProformaModel):
67
+ default_periods = [2024, 2025, 2026]
68
+
69
+ tax_rate = ScalarLine(value=0.21, label="Tax Rate")
70
+
71
+ revenue = FixedLine(
72
+ values={2024: 500_000, 2025: 550_000, 2026: 605_000},
73
+ label="Revenue",
74
+ tags=["operating"],
75
+ value_format=Format.CURRENCY_NO_DECIMALS,
76
+ )
77
+ cogs = FormulaLine(
78
+ formula=lambda li, t: li.revenue[t] * 0.55,
79
+ label="Cost of Goods Sold",
80
+ value_format=Format.CURRENCY_NO_DECIMALS,
81
+ )
82
+ gross_profit = FormulaLine(
83
+ formula=lambda li, t: li.revenue[t] - li.cogs[t],
84
+ label="Gross Profit",
85
+ value_format=Format.CURRENCY_NO_DECIMALS,
86
+ )
87
+ net_income = FormulaLine(
88
+ formula=lambda li, t: li.gross_profit[t] * (1 - li.tax_rate),
89
+ label="Net Income",
90
+ value_format=Format.CURRENCY_NO_DECIMALS,
91
+ )
92
+
93
+ model = IncomeStatement() # uses default_periods
94
+ ```
95
+
96
+ Access results with dot notation (primary) or bracket notation (useful when the name is in a variable):
97
+
98
+ ```python
99
+ model.net_income[2025] # 173_745.0 — dot notation
100
+ model["net_income"][2025] # same value — bracket notation
101
+
102
+ model.tax_rate.value # 0.21 — scalars have .value, not [t]
103
+
104
+ model.periods # [2024, 2025, 2026]
105
+ model.line_item_names # ["revenue", "cogs", "gross_profit", "net_income"]
106
+ model.scalar_names # ["tax_rate"]
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Line item types
112
+
113
+ | Type | Use |
114
+ |------|-----|
115
+ | `FixedLine(values, ...)` | Hardcoded values per period |
116
+ | `FormulaLine(formula, ...)` | Calculated from other items via a lambda |
117
+ | `ScalarLine(value, ...)` | A single value shared across all periods |
118
+ | `InputLine(default, ...)` | Period-indexed values supplied at instantiation |
119
+ | `ScalarInputLine(default, ...)` | A single value supplied at instantiation |
120
+
121
+ Formula lambdas receive `li` (the model namespace) and `t` (the current period). Period-indexed items use `li.name[t]`; scalars use `li.name` (no `[t]`). Reference prior periods with `li.name[t-1]`.
122
+
123
+ ---
124
+
125
+ ## Scenario inputs
126
+
127
+ `InputLine` and `ScalarInputLine` let callers supply values at instantiation without subclassing again — useful for scenario analysis.
128
+
129
+ ```python
130
+ from pyproforma import InputLine, ScalarInputLine
131
+
132
+ class FlexModel(ProformaModel):
133
+ default_periods = [2024, 2025, 2026]
134
+
135
+ margin = ScalarInputLine(default=0.45, label="Gross Margin")
136
+ revenue = FixedLine(
137
+ values={2024: 500_000, 2025: 550_000, 2026: 605_000},
138
+ label="Revenue",
139
+ value_format=Format.CURRENCY_NO_DECIMALS,
140
+ )
141
+ gross_profit = FormulaLine(
142
+ formula=lambda li, t: li.revenue[t] * li.margin,
143
+ label="Gross Profit",
144
+ value_format=Format.CURRENCY_NO_DECIMALS,
145
+ )
146
+
147
+ base = FlexModel() # uses default margin of 0.45
148
+ upside = FlexModel(margin=0.52) # override at instantiation
149
+ ```
150
+
151
+ Use `model.compare()` to diff two instances:
152
+
153
+ ```python
154
+ comparison = base.compare(upside, labels=["Base", "Upside"])
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Time-series formulas
160
+
161
+ Seed a value in the first period and let the formula compound from there — no `if t == first_year` guards needed:
162
+
163
+ ```python
164
+ revenue = FormulaLine(
165
+ formula=lambda li, t: li.revenue[t-1] * (1 + li.growth_rate),
166
+ values={2024: 500_000}, # engine uses this for 2024; formula runs from 2025 onward
167
+ label="Revenue",
168
+ )
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Tags
174
+
175
+ Tag line items to group them without fixed categories:
176
+
177
+ ```python
178
+ water_sales = FixedLine(values={...}, tags=["revenue"])
179
+ power_sales = FixedLine(values={...}, tags=["revenue"])
180
+
181
+ # Sum all "revenue"-tagged items in a formula
182
+ total_revenue = FormulaLine(formula=lambda li, t: li.tag["revenue"][t])
183
+ ```
184
+
185
+ Tags also work in table templates:
186
+
187
+ ```python
188
+ from pyproforma import TagTotalRow
189
+ TagTotalRow(tag="revenue", label="Total Revenue")
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Tables
195
+
196
+ Generate formatted tables for HTML, Excel, or pandas. `from_template` gives full control over layout:
197
+
198
+ ```python
199
+ from pyproforma import HeaderRow, LabelRow, ItemRow, BlankRow, LineItemsTotalRow
200
+
201
+ table = model.tables.from_template([
202
+ HeaderRow(),
203
+ LabelRow("Income Statement"),
204
+ ItemRow("revenue"),
205
+ ItemRow("cogs", reverse_sign=True), # display as positive deduction
206
+ ItemRow("gross_profit", bold=True, borders="top"),
207
+ BlankRow(),
208
+ ItemRow("net_income", bold=True),
209
+ ])
210
+
211
+ table.show() # inline in Jupyter
212
+ table.to_excel("output.xlsx") # Excel with formatting preserved
213
+ table.to_dataframe() # pandas DataFrame
214
+ ```
215
+
216
+ Convenience builders for common layouts:
217
+
218
+ ```python
219
+ model.tables.line_items().show() # all line items
220
+ model.tables.line_item("net_income", include_percent_change=True).show()
221
+ model.tables.precedents("net_income").show() # formula dependency tree
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Charts
227
+
228
+ ```python
229
+ model.charts.line_item("net_income", chart_type="bar").show()
230
+ model.charts.line_items(["revenue", "gross_profit", "net_income"]).show()
231
+ ```
232
+
233
+ Charts return a `ChartSpec` which can also render to a matplotlib `Figure`:
234
+
235
+ ```python
236
+ fig = model.charts.line_item("net_income").figure()
237
+ ```
238
+
239
+ Requires `pip install pyproforma[charts]`.
240
+
241
+ ---
242
+
243
+ ## Number formatting
244
+
245
+ Named format constants flow through to both HTML and Excel output:
246
+
247
+ ```python
248
+ from pyproforma import Format
249
+
250
+ ItemRow("revenue", value_format=Format.CURRENCY_NO_DECIMALS) # $500,000
251
+ ItemRow("revenue", value_format=Format.THOUSANDS_K) # 500.0K
252
+ ItemRow("margin", value_format=Format.PERCENT_ONE_DECIMAL) # 45.0%
253
+ ItemRow("net_income", value_format=Format.MILLIONS_M) # $0.2M
254
+ ```
255
+
256
+ Custom formats via `NumberFormatSpec`:
257
+
258
+ ```python
259
+ from pyproforma import NumberFormatSpec
260
+
261
+ fmt = NumberFormatSpec(decimals=1, scale="millions", prefix="$", suffix="M")
262
+ # 500_000 → "$0.5M"
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Explorer
268
+
269
+ A lightweight Flask web app for browsing any model interactively:
270
+
271
+ ```python
272
+ from pyproforma.explorer import create_app
273
+
274
+ app = create_app(model)
275
+ app.run(debug=True)
276
+ ```
277
+
278
+ Requires `pip install pyproforma[explorer]`. The app shows all line items, their values, formula sources, and lets you update `InputLine` / `ScalarInputLine` values live. You can also pass named tables, charts, and views to build a richer dashboard.
279
+
280
+ ### CLI
281
+
282
+ Launch the explorer straight from the command line, no wrapper script needed — point it at a `.py` file with a module-level `model`:
283
+
284
+ ```bash
285
+ pyproforma my_model.py
286
+ ```
287
+
288
+ Pass `-c config.yaml` to drive tables, charts, views, and a home page from a YAML file instead of Python:
289
+
290
+ ```yaml
291
+ tables:
292
+ Income Statement:
293
+ rows:
294
+ - row_type: header
295
+ - row_type: item
296
+ name: revenue
297
+ - row_type: item
298
+ name: net_income
299
+ bold: true
300
+ top_border: single
301
+
302
+ charts:
303
+ Revenue vs Net Income:
304
+ names: [revenue, net_income]
305
+
306
+ views:
307
+ Overview:
308
+ - - type: chart
309
+ ref: Revenue vs Net Income
310
+ - type: table
311
+ ref: Income Statement
312
+
313
+ home_view: Overview
314
+ ```
315
+
316
+ ```bash
317
+ pyproforma my_model.py -c config.yaml
318
+ ```
319
+
320
+ ### Scenarios & compare mode
321
+
322
+ Add a `scenarios:` block (each entry maps to constructor kwargs, e.g. `InputLine` / `ScalarInputLine` overrides) and the CLI switches to a multi-model app — a "Scenario" dropdown lets you browse each one read-only, plus an "All (Compare)" view:
323
+
324
+ ```yaml
325
+ scenarios:
326
+ Upside:
327
+ margin: 0.52
328
+ Downside:
329
+ margin: 0.38
330
+ ```
331
+
332
+ Add an optional `compare:` block to curate cross-scenario tables/charts/views (built on `ModelComparison` under the hood — value rows, absolute/percent difference rows, per-scenario chart series):
333
+
334
+ ```yaml
335
+ compare:
336
+ tables:
337
+ Margin Impact:
338
+ items: [gross_profit, net_income]
339
+ charts:
340
+ Net Income: { item: net_income, chart_type: bar }
341
+ views:
342
+ Summary:
343
+ - - { type: chart, ref: "Net Income" }
344
+ - - { type: table, ref: "Margin Impact" }
345
+ ```
346
+
347
+ Without a config file, compare mode still works from Python:
348
+
349
+ ```python
350
+ from pyproforma.compare import ModelComparison
351
+
352
+ comparison = ModelComparison(base, upside, downside, labels=["Base", "Upside", "Downside"])
353
+ comparison.table(["revenue", "net_income"]) # value + difference rows, one column per period
354
+ comparison.chart("net_income").show() # one series per model
355
+ ```
356
+
357
+ ---
358
+
359
+ ## Installation
360
+
361
+ ```bash
362
+ pip install pyproforma # core only
363
+ pip install pyproforma[charts] # + matplotlib
364
+ pip install pyproforma[excel] # + openpyxl
365
+ pip install pyproforma[explorer] # + Flask
366
+ pip install pyproforma[pandas] # + pandas
367
+ ```
368
+
369
+ Requires Python 3.9+.
370
+
371
+ ---
372
+
373
+ ## Status
374
+
375
+ Active development. Core modeling, table export, charts, and the Flask explorer are all stable. Feedback welcome — open an issue on GitHub.
376
+
377
+ ## License
378
+
379
+ MIT
@@ -0,0 +1,337 @@
1
+ # pyproforma
2
+
3
+ A Python library for building financial models — a code-first alternative to Excel for pro formas, projections, and structured financial tables.
4
+
5
+ ```bash
6
+ pip install pyproforma
7
+ ```
8
+
9
+ ---
10
+
11
+ ## Why
12
+
13
+ Spreadsheets are the default tool for financial modeling, but they have real problems: no version control, no testing, formulas hidden inside cells, and no easy way to generate the same model for multiple scenarios. pyproforma is designed for analysts who want the benefits of code — reproducibility, testability, version history — without giving up the tabular output that finance people actually use.
14
+
15
+ ---
16
+
17
+ ## How it works
18
+
19
+ Define a model by subclassing `ProformaModel` and declaring line items as class attributes. Instantiate it with a list of periods and the library calculates everything.
20
+
21
+ ```python
22
+ from pyproforma import ProformaModel, FixedLine, FormulaLine, ScalarLine, Format
23
+
24
+ class IncomeStatement(ProformaModel):
25
+ default_periods = [2024, 2025, 2026]
26
+
27
+ tax_rate = ScalarLine(value=0.21, label="Tax Rate")
28
+
29
+ revenue = FixedLine(
30
+ values={2024: 500_000, 2025: 550_000, 2026: 605_000},
31
+ label="Revenue",
32
+ tags=["operating"],
33
+ value_format=Format.CURRENCY_NO_DECIMALS,
34
+ )
35
+ cogs = FormulaLine(
36
+ formula=lambda li, t: li.revenue[t] * 0.55,
37
+ label="Cost of Goods Sold",
38
+ value_format=Format.CURRENCY_NO_DECIMALS,
39
+ )
40
+ gross_profit = FormulaLine(
41
+ formula=lambda li, t: li.revenue[t] - li.cogs[t],
42
+ label="Gross Profit",
43
+ value_format=Format.CURRENCY_NO_DECIMALS,
44
+ )
45
+ net_income = FormulaLine(
46
+ formula=lambda li, t: li.gross_profit[t] * (1 - li.tax_rate),
47
+ label="Net Income",
48
+ value_format=Format.CURRENCY_NO_DECIMALS,
49
+ )
50
+
51
+ model = IncomeStatement() # uses default_periods
52
+ ```
53
+
54
+ Access results with dot notation (primary) or bracket notation (useful when the name is in a variable):
55
+
56
+ ```python
57
+ model.net_income[2025] # 173_745.0 — dot notation
58
+ model["net_income"][2025] # same value — bracket notation
59
+
60
+ model.tax_rate.value # 0.21 — scalars have .value, not [t]
61
+
62
+ model.periods # [2024, 2025, 2026]
63
+ model.line_item_names # ["revenue", "cogs", "gross_profit", "net_income"]
64
+ model.scalar_names # ["tax_rate"]
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Line item types
70
+
71
+ | Type | Use |
72
+ |------|-----|
73
+ | `FixedLine(values, ...)` | Hardcoded values per period |
74
+ | `FormulaLine(formula, ...)` | Calculated from other items via a lambda |
75
+ | `ScalarLine(value, ...)` | A single value shared across all periods |
76
+ | `InputLine(default, ...)` | Period-indexed values supplied at instantiation |
77
+ | `ScalarInputLine(default, ...)` | A single value supplied at instantiation |
78
+
79
+ Formula lambdas receive `li` (the model namespace) and `t` (the current period). Period-indexed items use `li.name[t]`; scalars use `li.name` (no `[t]`). Reference prior periods with `li.name[t-1]`.
80
+
81
+ ---
82
+
83
+ ## Scenario inputs
84
+
85
+ `InputLine` and `ScalarInputLine` let callers supply values at instantiation without subclassing again — useful for scenario analysis.
86
+
87
+ ```python
88
+ from pyproforma import InputLine, ScalarInputLine
89
+
90
+ class FlexModel(ProformaModel):
91
+ default_periods = [2024, 2025, 2026]
92
+
93
+ margin = ScalarInputLine(default=0.45, label="Gross Margin")
94
+ revenue = FixedLine(
95
+ values={2024: 500_000, 2025: 550_000, 2026: 605_000},
96
+ label="Revenue",
97
+ value_format=Format.CURRENCY_NO_DECIMALS,
98
+ )
99
+ gross_profit = FormulaLine(
100
+ formula=lambda li, t: li.revenue[t] * li.margin,
101
+ label="Gross Profit",
102
+ value_format=Format.CURRENCY_NO_DECIMALS,
103
+ )
104
+
105
+ base = FlexModel() # uses default margin of 0.45
106
+ upside = FlexModel(margin=0.52) # override at instantiation
107
+ ```
108
+
109
+ Use `model.compare()` to diff two instances:
110
+
111
+ ```python
112
+ comparison = base.compare(upside, labels=["Base", "Upside"])
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Time-series formulas
118
+
119
+ Seed a value in the first period and let the formula compound from there — no `if t == first_year` guards needed:
120
+
121
+ ```python
122
+ revenue = FormulaLine(
123
+ formula=lambda li, t: li.revenue[t-1] * (1 + li.growth_rate),
124
+ values={2024: 500_000}, # engine uses this for 2024; formula runs from 2025 onward
125
+ label="Revenue",
126
+ )
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Tags
132
+
133
+ Tag line items to group them without fixed categories:
134
+
135
+ ```python
136
+ water_sales = FixedLine(values={...}, tags=["revenue"])
137
+ power_sales = FixedLine(values={...}, tags=["revenue"])
138
+
139
+ # Sum all "revenue"-tagged items in a formula
140
+ total_revenue = FormulaLine(formula=lambda li, t: li.tag["revenue"][t])
141
+ ```
142
+
143
+ Tags also work in table templates:
144
+
145
+ ```python
146
+ from pyproforma import TagTotalRow
147
+ TagTotalRow(tag="revenue", label="Total Revenue")
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Tables
153
+
154
+ Generate formatted tables for HTML, Excel, or pandas. `from_template` gives full control over layout:
155
+
156
+ ```python
157
+ from pyproforma import HeaderRow, LabelRow, ItemRow, BlankRow, LineItemsTotalRow
158
+
159
+ table = model.tables.from_template([
160
+ HeaderRow(),
161
+ LabelRow("Income Statement"),
162
+ ItemRow("revenue"),
163
+ ItemRow("cogs", reverse_sign=True), # display as positive deduction
164
+ ItemRow("gross_profit", bold=True, borders="top"),
165
+ BlankRow(),
166
+ ItemRow("net_income", bold=True),
167
+ ])
168
+
169
+ table.show() # inline in Jupyter
170
+ table.to_excel("output.xlsx") # Excel with formatting preserved
171
+ table.to_dataframe() # pandas DataFrame
172
+ ```
173
+
174
+ Convenience builders for common layouts:
175
+
176
+ ```python
177
+ model.tables.line_items().show() # all line items
178
+ model.tables.line_item("net_income", include_percent_change=True).show()
179
+ model.tables.precedents("net_income").show() # formula dependency tree
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Charts
185
+
186
+ ```python
187
+ model.charts.line_item("net_income", chart_type="bar").show()
188
+ model.charts.line_items(["revenue", "gross_profit", "net_income"]).show()
189
+ ```
190
+
191
+ Charts return a `ChartSpec` which can also render to a matplotlib `Figure`:
192
+
193
+ ```python
194
+ fig = model.charts.line_item("net_income").figure()
195
+ ```
196
+
197
+ Requires `pip install pyproforma[charts]`.
198
+
199
+ ---
200
+
201
+ ## Number formatting
202
+
203
+ Named format constants flow through to both HTML and Excel output:
204
+
205
+ ```python
206
+ from pyproforma import Format
207
+
208
+ ItemRow("revenue", value_format=Format.CURRENCY_NO_DECIMALS) # $500,000
209
+ ItemRow("revenue", value_format=Format.THOUSANDS_K) # 500.0K
210
+ ItemRow("margin", value_format=Format.PERCENT_ONE_DECIMAL) # 45.0%
211
+ ItemRow("net_income", value_format=Format.MILLIONS_M) # $0.2M
212
+ ```
213
+
214
+ Custom formats via `NumberFormatSpec`:
215
+
216
+ ```python
217
+ from pyproforma import NumberFormatSpec
218
+
219
+ fmt = NumberFormatSpec(decimals=1, scale="millions", prefix="$", suffix="M")
220
+ # 500_000 → "$0.5M"
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Explorer
226
+
227
+ A lightweight Flask web app for browsing any model interactively:
228
+
229
+ ```python
230
+ from pyproforma.explorer import create_app
231
+
232
+ app = create_app(model)
233
+ app.run(debug=True)
234
+ ```
235
+
236
+ Requires `pip install pyproforma[explorer]`. The app shows all line items, their values, formula sources, and lets you update `InputLine` / `ScalarInputLine` values live. You can also pass named tables, charts, and views to build a richer dashboard.
237
+
238
+ ### CLI
239
+
240
+ Launch the explorer straight from the command line, no wrapper script needed — point it at a `.py` file with a module-level `model`:
241
+
242
+ ```bash
243
+ pyproforma my_model.py
244
+ ```
245
+
246
+ Pass `-c config.yaml` to drive tables, charts, views, and a home page from a YAML file instead of Python:
247
+
248
+ ```yaml
249
+ tables:
250
+ Income Statement:
251
+ rows:
252
+ - row_type: header
253
+ - row_type: item
254
+ name: revenue
255
+ - row_type: item
256
+ name: net_income
257
+ bold: true
258
+ top_border: single
259
+
260
+ charts:
261
+ Revenue vs Net Income:
262
+ names: [revenue, net_income]
263
+
264
+ views:
265
+ Overview:
266
+ - - type: chart
267
+ ref: Revenue vs Net Income
268
+ - type: table
269
+ ref: Income Statement
270
+
271
+ home_view: Overview
272
+ ```
273
+
274
+ ```bash
275
+ pyproforma my_model.py -c config.yaml
276
+ ```
277
+
278
+ ### Scenarios & compare mode
279
+
280
+ Add a `scenarios:` block (each entry maps to constructor kwargs, e.g. `InputLine` / `ScalarInputLine` overrides) and the CLI switches to a multi-model app — a "Scenario" dropdown lets you browse each one read-only, plus an "All (Compare)" view:
281
+
282
+ ```yaml
283
+ scenarios:
284
+ Upside:
285
+ margin: 0.52
286
+ Downside:
287
+ margin: 0.38
288
+ ```
289
+
290
+ Add an optional `compare:` block to curate cross-scenario tables/charts/views (built on `ModelComparison` under the hood — value rows, absolute/percent difference rows, per-scenario chart series):
291
+
292
+ ```yaml
293
+ compare:
294
+ tables:
295
+ Margin Impact:
296
+ items: [gross_profit, net_income]
297
+ charts:
298
+ Net Income: { item: net_income, chart_type: bar }
299
+ views:
300
+ Summary:
301
+ - - { type: chart, ref: "Net Income" }
302
+ - - { type: table, ref: "Margin Impact" }
303
+ ```
304
+
305
+ Without a config file, compare mode still works from Python:
306
+
307
+ ```python
308
+ from pyproforma.compare import ModelComparison
309
+
310
+ comparison = ModelComparison(base, upside, downside, labels=["Base", "Upside", "Downside"])
311
+ comparison.table(["revenue", "net_income"]) # value + difference rows, one column per period
312
+ comparison.chart("net_income").show() # one series per model
313
+ ```
314
+
315
+ ---
316
+
317
+ ## Installation
318
+
319
+ ```bash
320
+ pip install pyproforma # core only
321
+ pip install pyproforma[charts] # + matplotlib
322
+ pip install pyproforma[excel] # + openpyxl
323
+ pip install pyproforma[explorer] # + Flask
324
+ pip install pyproforma[pandas] # + pandas
325
+ ```
326
+
327
+ Requires Python 3.9+.
328
+
329
+ ---
330
+
331
+ ## Status
332
+
333
+ Active development. Core modeling, table export, charts, and the Flask explorer are all stable. Feedback welcome — open an issue on GitHub.
334
+
335
+ ## License
336
+
337
+ MIT