pyproforma 0.2.1__tar.gz → 0.2.2__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.
- {pyproforma-0.2.1/pyproforma.egg-info → pyproforma-0.2.2}/PKG-INFO +3 -1
- pyproforma-0.2.2/pyproforma/explorer/__init__.py +3 -0
- pyproforma-0.2.2/pyproforma/explorer/app.py +340 -0
- pyproforma-0.2.2/pyproforma/explorer/components.py +42 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2/pyproforma.egg-info}/PKG-INFO +3 -1
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma.egg-info/SOURCES.txt +3 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma.egg-info/requires.txt +3 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproject.toml +4 -1
- {pyproforma-0.2.1 → pyproforma-0.2.2}/LICENSE +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/MANIFEST.in +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/README.md +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/__init__.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/calculation_engine.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/chart/__init__.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/chart/chart.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/chart/renderers/__init__.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/chart/renderers/base.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/chart/renderers/matplotlib_renderer.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/charts/__init__.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/charts/chart_def.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/charts/charts.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/compare/__init__.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/compare/model_comparison.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/line_items/__init__.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/line_items/debt_line.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/line_items/fixed_line.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/line_items/formula_line.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/line_items/input_line.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/line_items/line_item.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/line_items/line_item_result.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/line_items/line_item_selection.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/line_items/line_item_values.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/model_namespace.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/proforma_model.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/reserved_words.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/table/__init__.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/table/bootstrap_html_renderer.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/table/colors.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/table/excel.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/table/format_value.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/table/html_renderer.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/table/table_class.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/tables/__init__.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/tables/row_types.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/tables/table_def.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/tables/tables.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma/tags_namespace.py +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma.egg-info/dependency_links.txt +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/pyproforma.egg-info/top_level.txt +0 -0
- {pyproforma-0.2.1 → pyproforma-0.2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyproforma
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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>
|
|
@@ -33,6 +33,8 @@ Requires-Dist: pytest>=6.0; extra == "dev"
|
|
|
33
33
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
34
34
|
Provides-Extra: notebook
|
|
35
35
|
Requires-Dist: ipython>=7.0.0; extra == "notebook"
|
|
36
|
+
Provides-Extra: explorer
|
|
37
|
+
Requires-Dist: flask>=2.0.0; extra == "explorer"
|
|
36
38
|
Dynamic: license-file
|
|
37
39
|
|
|
38
40
|
# pyproforma
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Flask app factory for exploring a ProformaModel in a web browser."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from flask import Flask, abort, flash, redirect, render_template, request, url_for
|
|
8
|
+
|
|
9
|
+
from pyproforma.explorer.components import StatCard
|
|
10
|
+
from pyproforma.line_items.fixed_line import FixedLine
|
|
11
|
+
from pyproforma.line_items.formula_line import FormulaLine
|
|
12
|
+
from pyproforma.line_items.input_line import InputLine
|
|
13
|
+
from pyproforma.table import Format
|
|
14
|
+
from pyproforma.tables.row_types import HeaderRow, ItemRow, TagItemsRow
|
|
15
|
+
from pyproforma.tables.table_def import TableDef
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_app(model, tables=None, charts=None, views=None):
|
|
19
|
+
"""Create a Flask app for exploring a ProformaModel.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
model: An instantiated ProformaModel.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Flask app instance.
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
from pyproforma import ProformaModel, FixedLine
|
|
29
|
+
from pyproforma.explorer import create_app
|
|
30
|
+
|
|
31
|
+
class MyModel(ProformaModel):
|
|
32
|
+
revenue = FixedLine(values={2024: 100_000, 2025: 110_000})
|
|
33
|
+
|
|
34
|
+
model = MyModel(periods=[2024, 2025])
|
|
35
|
+
app = create_app(model)
|
|
36
|
+
app.run(debug=True)
|
|
37
|
+
"""
|
|
38
|
+
app = Flask(__name__, template_folder=os.path.join(os.path.dirname(__file__), "templates"))
|
|
39
|
+
app.secret_key = "pyproforma-explorer"
|
|
40
|
+
|
|
41
|
+
class _State:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
state = _State()
|
|
45
|
+
state.model = model
|
|
46
|
+
state.model_class = type(model)
|
|
47
|
+
state.periods = model.periods
|
|
48
|
+
state.error = None
|
|
49
|
+
|
|
50
|
+
all_items_def = TableDef(
|
|
51
|
+
rows=[HeaderRow(), *[ItemRow(name=n) for n in model.line_item_names]],
|
|
52
|
+
title="All Line Items",
|
|
53
|
+
)
|
|
54
|
+
state.tables = {"All Line Items": all_items_def, **(tables or {})}
|
|
55
|
+
state.charts = charts or {}
|
|
56
|
+
state.views = views or {}
|
|
57
|
+
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
# Helpers
|
|
60
|
+
# ------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
@app.context_processor
|
|
63
|
+
def inject_nav():
|
|
64
|
+
return {
|
|
65
|
+
"nav_tables": list(enumerate(state.tables.keys())),
|
|
66
|
+
"nav_charts": list(enumerate(state.charts.keys())),
|
|
67
|
+
"nav_views": list(enumerate(state.views.keys())),
|
|
68
|
+
"nav_tags": state.model.tags,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def _format_name(spec):
|
|
72
|
+
if spec is None:
|
|
73
|
+
return "—"
|
|
74
|
+
for name, standard in Format._STRING_MAP.items():
|
|
75
|
+
if spec == standard:
|
|
76
|
+
return name.upper()
|
|
77
|
+
return str(spec)
|
|
78
|
+
|
|
79
|
+
def _build_items(names):
|
|
80
|
+
m = state.model
|
|
81
|
+
items = []
|
|
82
|
+
for name in names:
|
|
83
|
+
item_def = getattr(type(m), name)
|
|
84
|
+
is_scalar = isinstance(item_def, FixedLine) and item_def.is_scalar
|
|
85
|
+
items.append({
|
|
86
|
+
"name": name,
|
|
87
|
+
"label": item_def.label or name,
|
|
88
|
+
"type": type(item_def).__name__,
|
|
89
|
+
"tags": item_def.tags,
|
|
90
|
+
"scalar": is_scalar,
|
|
91
|
+
"value": m[name].formatted_value(m.periods[0]) if (is_scalar and m.periods) else None,
|
|
92
|
+
})
|
|
93
|
+
return items
|
|
94
|
+
|
|
95
|
+
def _build_inputs():
|
|
96
|
+
m = state.model
|
|
97
|
+
inputs = []
|
|
98
|
+
for name in state.model_class._input_line_names:
|
|
99
|
+
spec = getattr(state.model_class, name)
|
|
100
|
+
is_scalar = name in m._scalars
|
|
101
|
+
if is_scalar:
|
|
102
|
+
formatted = [m[name].formatted_value(p) for p in state.periods[:1]]
|
|
103
|
+
else:
|
|
104
|
+
formatted = [m[name].formatted_value(p) for p in state.periods]
|
|
105
|
+
inputs.append({
|
|
106
|
+
"name": name,
|
|
107
|
+
"label": spec.label or name,
|
|
108
|
+
"is_scalar": is_scalar,
|
|
109
|
+
"value": m._scalars[name] if is_scalar else m._input_line_values.get(name, {}),
|
|
110
|
+
"formatted_values": formatted,
|
|
111
|
+
})
|
|
112
|
+
return inputs
|
|
113
|
+
|
|
114
|
+
def _add_hrefs(definition):
|
|
115
|
+
rows = definition.rows if isinstance(definition, TableDef) else definition
|
|
116
|
+
result = []
|
|
117
|
+
for row in rows:
|
|
118
|
+
if isinstance(row, ItemRow):
|
|
119
|
+
result.append(dataclasses.replace(row, href=url_for("line_item", name=row.name)))
|
|
120
|
+
elif isinstance(row, TagItemsRow):
|
|
121
|
+
names = [n for n in state.model.line_item_names
|
|
122
|
+
if row.tag in getattr(type(state.model), n).tags]
|
|
123
|
+
for name in names:
|
|
124
|
+
result.append(ItemRow(name=name, bold=row.bold,
|
|
125
|
+
href=url_for("line_item", name=name)))
|
|
126
|
+
else:
|
|
127
|
+
result.append(row)
|
|
128
|
+
if isinstance(definition, TableDef):
|
|
129
|
+
return TableDef(rows=result, title=definition.title)
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
# Routes
|
|
134
|
+
# ------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
@app.route("/")
|
|
137
|
+
def index():
|
|
138
|
+
m = state.model
|
|
139
|
+
return render_template(
|
|
140
|
+
"index.html",
|
|
141
|
+
model=m,
|
|
142
|
+
items=_build_items(m.line_item_names),
|
|
143
|
+
title=m.__class__.__name__,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
@app.route("/tag/<tag_name>")
|
|
147
|
+
def tag_view(tag_name):
|
|
148
|
+
m = state.model
|
|
149
|
+
names = [n for n in m.line_item_names if tag_name in getattr(type(m), n).tags]
|
|
150
|
+
if not names:
|
|
151
|
+
abort(404)
|
|
152
|
+
|
|
153
|
+
tag_template = [
|
|
154
|
+
HeaderRow(),
|
|
155
|
+
TagItemsRow(tag=tag_name, include_total_row=True,
|
|
156
|
+
total_row_label=f"Total {tag_name}"),
|
|
157
|
+
]
|
|
158
|
+
tag_table_html = m.tables.build(_add_hrefs(tag_template)).to_bootstrap_html()
|
|
159
|
+
tag_chart_data = json.dumps(
|
|
160
|
+
m.charts.line_items(names, chart_type="stacked_bar", title=tag_name).to_apexcharts()
|
|
161
|
+
) if m.periods else None
|
|
162
|
+
|
|
163
|
+
return render_template(
|
|
164
|
+
"index.html",
|
|
165
|
+
model=m,
|
|
166
|
+
items=_build_items(names),
|
|
167
|
+
title=f"Tag: {tag_name}",
|
|
168
|
+
back_link=True,
|
|
169
|
+
tag_table_html=tag_table_html,
|
|
170
|
+
tag_chart_data=tag_chart_data,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
@app.route("/line_item/<name>")
|
|
174
|
+
def line_item(name):
|
|
175
|
+
m = state.model
|
|
176
|
+
if name not in m.line_item_names:
|
|
177
|
+
abort(404)
|
|
178
|
+
item_def = getattr(type(m), name)
|
|
179
|
+
result = m[name]
|
|
180
|
+
|
|
181
|
+
info = {
|
|
182
|
+
"name": name,
|
|
183
|
+
"label": item_def.label or name,
|
|
184
|
+
"type": type(item_def).__name__,
|
|
185
|
+
"tags": item_def.tags,
|
|
186
|
+
"value_format": _format_name(item_def.value_format),
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if isinstance(item_def, FormulaLine):
|
|
190
|
+
info["formula_source"] = item_def.formula_source
|
|
191
|
+
info["dependencies"] = item_def.precedents or []
|
|
192
|
+
info["tag_dependencies"] = {
|
|
193
|
+
tag: m.tag[tag].names
|
|
194
|
+
for tag in (item_def.tag_references or [])
|
|
195
|
+
}
|
|
196
|
+
elif isinstance(item_def, FixedLine):
|
|
197
|
+
if item_def.is_scalar:
|
|
198
|
+
info["scalar_value"] = result.formatted_value(m.periods[0]) if m.periods else str(item_def._scalar_value)
|
|
199
|
+
else:
|
|
200
|
+
info["fixed_values"] = item_def.values or {}
|
|
201
|
+
elif isinstance(item_def, InputLine):
|
|
202
|
+
is_scalar = name in m._scalars
|
|
203
|
+
if is_scalar:
|
|
204
|
+
info["scalar_value"] = result.formatted_value(m.periods[0]) if m.periods else str(m._scalars[name])
|
|
205
|
+
else:
|
|
206
|
+
info["input_values"] = m._input_line_values.get(name, {})
|
|
207
|
+
info["is_input"] = True
|
|
208
|
+
|
|
209
|
+
values_table = None
|
|
210
|
+
scalar_input = isinstance(item_def, InputLine) and name in m._scalars
|
|
211
|
+
scalar_fixed = isinstance(item_def, FixedLine) and item_def.is_scalar
|
|
212
|
+
if not (scalar_input or scalar_fixed):
|
|
213
|
+
values_table = m.tables.line_item(name).to_bootstrap_html()
|
|
214
|
+
|
|
215
|
+
precedents_table = None
|
|
216
|
+
if isinstance(item_def, FormulaLine) and item_def.precedents:
|
|
217
|
+
precedents_table = m.tables.precedents(name).to_bootstrap_html()
|
|
218
|
+
|
|
219
|
+
chart_data = json.dumps(m.charts.line_item(name).to_apexcharts()) if m.periods else None
|
|
220
|
+
dependents = m.dependents(name)
|
|
221
|
+
|
|
222
|
+
return render_template(
|
|
223
|
+
"line_item.html",
|
|
224
|
+
model=m,
|
|
225
|
+
info=info,
|
|
226
|
+
values_table=values_table,
|
|
227
|
+
precedents_table=precedents_table,
|
|
228
|
+
chart_data=chart_data,
|
|
229
|
+
dependents=dependents,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
@app.route("/table/<int:idx>")
|
|
233
|
+
def table_view(idx):
|
|
234
|
+
labels = list(state.tables.keys())
|
|
235
|
+
if idx >= len(labels):
|
|
236
|
+
abort(404)
|
|
237
|
+
label = labels[idx]
|
|
238
|
+
definition = state.tables[label]
|
|
239
|
+
table = state.model.tables.build(_add_hrefs(definition))
|
|
240
|
+
return render_template(
|
|
241
|
+
"table_view.html",
|
|
242
|
+
model=state.model,
|
|
243
|
+
title=table.title or label,
|
|
244
|
+
table_html=table.to_bootstrap_html(),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
@app.route("/chart/<int:idx>")
|
|
248
|
+
def chart_view(idx):
|
|
249
|
+
labels = list(state.charts.keys())
|
|
250
|
+
if idx >= len(labels):
|
|
251
|
+
abort(404)
|
|
252
|
+
label = labels[idx]
|
|
253
|
+
chart_data = json.dumps(state.model.charts.build(state.charts[label]).to_apexcharts())
|
|
254
|
+
return render_template(
|
|
255
|
+
"chart_view.html",
|
|
256
|
+
model=state.model,
|
|
257
|
+
title=label,
|
|
258
|
+
chart_data=chart_data,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
@app.route("/view/<int:idx>")
|
|
262
|
+
def view_page(idx):
|
|
263
|
+
labels = list(state.views.keys())
|
|
264
|
+
if idx >= len(labels):
|
|
265
|
+
abort(404)
|
|
266
|
+
label = labels[idx]
|
|
267
|
+
view_def = state.views[label]
|
|
268
|
+
|
|
269
|
+
rows = []
|
|
270
|
+
for row_idx, row in enumerate(view_def):
|
|
271
|
+
col_width = 12 // len(row)
|
|
272
|
+
processed = []
|
|
273
|
+
for col_idx, comp in enumerate(row):
|
|
274
|
+
if isinstance(comp, StatCard):
|
|
275
|
+
c = comp.build(state.model)
|
|
276
|
+
c["col_width"] = col_width
|
|
277
|
+
elif isinstance(comp, dict) and comp.get("type") == "stat":
|
|
278
|
+
c = StatCard(
|
|
279
|
+
name=comp["name"],
|
|
280
|
+
label=comp.get("label"),
|
|
281
|
+
aggregation=comp.get("aggregation", "latest"),
|
|
282
|
+
).build(state.model)
|
|
283
|
+
c["col_width"] = col_width
|
|
284
|
+
elif comp["type"] == "chart":
|
|
285
|
+
c = dict(comp)
|
|
286
|
+
c["col_width"] = col_width
|
|
287
|
+
c["chart_data"] = json.dumps(
|
|
288
|
+
state.model.charts.build(state.charts[comp["ref"]]).to_apexcharts()
|
|
289
|
+
)
|
|
290
|
+
c["chart_id"] = f"view-chart-{row_idx}-{col_idx}"
|
|
291
|
+
elif comp["type"] == "table":
|
|
292
|
+
built = state.model.tables.build(_add_hrefs(state.tables[comp["ref"]]))
|
|
293
|
+
c = dict(comp)
|
|
294
|
+
c["col_width"] = col_width
|
|
295
|
+
c["html"] = built.to_bootstrap_html()
|
|
296
|
+
c["table_title"] = built.title or comp["ref"]
|
|
297
|
+
processed.append(c)
|
|
298
|
+
rows.append(processed)
|
|
299
|
+
|
|
300
|
+
return render_template(
|
|
301
|
+
"view.html",
|
|
302
|
+
model=state.model,
|
|
303
|
+
title=label,
|
|
304
|
+
rows=rows,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
@app.route("/inputs", methods=["GET"])
|
|
308
|
+
def inputs():
|
|
309
|
+
m = state.model
|
|
310
|
+
error = state.error
|
|
311
|
+
state.error = None
|
|
312
|
+
return render_template(
|
|
313
|
+
"inputs.html",
|
|
314
|
+
model=m,
|
|
315
|
+
inputs=_build_inputs(),
|
|
316
|
+
error=error,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
@app.route("/inputs", methods=["POST"])
|
|
320
|
+
def update_inputs():
|
|
321
|
+
m = state.model
|
|
322
|
+
kwargs = {}
|
|
323
|
+
try:
|
|
324
|
+
for name in state.model_class._input_line_names:
|
|
325
|
+
is_scalar = name in m._scalars
|
|
326
|
+
if is_scalar:
|
|
327
|
+
kwargs[name] = float(request.form[name])
|
|
328
|
+
else:
|
|
329
|
+
kwargs[name] = {
|
|
330
|
+
period: float(request.form[f"{name}_{period}"])
|
|
331
|
+
for period in state.periods
|
|
332
|
+
}
|
|
333
|
+
state.model = state.model_class(periods=state.periods, **kwargs)
|
|
334
|
+
state.error = None
|
|
335
|
+
flash("Model updated.")
|
|
336
|
+
except Exception as e:
|
|
337
|
+
state.error = str(e)
|
|
338
|
+
return redirect(url_for("inputs"))
|
|
339
|
+
|
|
340
|
+
return app
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Explorer components — declarative definitions for view components.
|
|
3
|
+
|
|
4
|
+
Each component has a build(model) method that returns a dict of render data
|
|
5
|
+
consumed by the view template. The template handles all HTML.
|
|
6
|
+
|
|
7
|
+
Components:
|
|
8
|
+
StatCard — displays a single aggregated line item value (min, max, first, latest).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class StatCard:
|
|
16
|
+
"""
|
|
17
|
+
Displays a single aggregated value from a line item.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
name: Line item name.
|
|
21
|
+
label: Display label. Defaults to the line item's own label.
|
|
22
|
+
aggregation: One of "min", "max", "latest", "first". Defaults to "latest".
|
|
23
|
+
value_format: Optional format override. Uses the line item's format if not set.
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
>>> StatCard("dscr", "Min DSCR", aggregation="min")
|
|
27
|
+
>>> StatCard("ending_cash", aggregation="latest")
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
label: str | None = None
|
|
32
|
+
aggregation: str = "latest"
|
|
33
|
+
value_format: object = None
|
|
34
|
+
|
|
35
|
+
def build(self, model) -> dict:
|
|
36
|
+
result = model[self.name]
|
|
37
|
+
formatted = getattr(result, f"formatted_{self.aggregation}")(self.value_format)
|
|
38
|
+
return {
|
|
39
|
+
"type": "stat",
|
|
40
|
+
"label": self.label or result.label or self.name,
|
|
41
|
+
"value": formatted,
|
|
42
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyproforma
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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>
|
|
@@ -33,6 +33,8 @@ Requires-Dist: pytest>=6.0; extra == "dev"
|
|
|
33
33
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
34
34
|
Provides-Extra: notebook
|
|
35
35
|
Requires-Dist: ipython>=7.0.0; extra == "notebook"
|
|
36
|
+
Provides-Extra: explorer
|
|
37
|
+
Requires-Dist: flask>=2.0.0; extra == "explorer"
|
|
36
38
|
Dynamic: license-file
|
|
37
39
|
|
|
38
40
|
# pyproforma
|
|
@@ -23,6 +23,9 @@ pyproforma/charts/chart_def.py
|
|
|
23
23
|
pyproforma/charts/charts.py
|
|
24
24
|
pyproforma/compare/__init__.py
|
|
25
25
|
pyproforma/compare/model_comparison.py
|
|
26
|
+
pyproforma/explorer/__init__.py
|
|
27
|
+
pyproforma/explorer/app.py
|
|
28
|
+
pyproforma/explorer/components.py
|
|
26
29
|
pyproforma/line_items/__init__.py
|
|
27
30
|
pyproforma/line_items/debt_line.py
|
|
28
31
|
pyproforma/line_items/fixed_line.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyproforma"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "A Python package for financial modeling and reporting"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -50,6 +50,9 @@ dev = [
|
|
|
50
50
|
notebook = [
|
|
51
51
|
"ipython>=7.0.0",
|
|
52
52
|
]
|
|
53
|
+
explorer = [
|
|
54
|
+
"flask>=2.0.0",
|
|
55
|
+
]
|
|
53
56
|
|
|
54
57
|
[tool.setuptools.packages.find]
|
|
55
58
|
where = ["."]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|