Skip to content

Commit 8b9f7ee

Browse files
committed
refactor: move recommendations in copilot submodule
1 parent efaa8b8 commit 8b9f7ee

File tree

4 files changed

+103
-88
lines changed

4 files changed

+103
-88
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Don't have Finary yet? You can sign up using my [referral link](https://finary.c
2626
🇫🇷🥖 Vous pouvez traduire cette page en Français avec votre navigateur (_clic droit > traduire_).
2727

2828
<p align="center">
29-
<img src="https://raw.githubusercontent.com/MadeInPierre/finalynx/main/docs/_static/screenshot_demo.png" width="600" />
29+
<img src="https://raw.githubusercontent.com/MadeInPierre/finalynx/main/docs/_static/screenshot_demo_frameless.png" width="600" />
3030
</p>
3131

3232
## ✨ Features
@@ -65,8 +65,8 @@ Here is the bare minimum code accepted:
6565

6666
```python
6767
from finalynx import Portfolio, Assistant
68-
portfolio = Portfolio() # <- your custom configuration here
69-
Assistant(portfolio).run()
68+
portfolio = Portfolio() # <- your custom configuration here
69+
Assistant(portfolio).run() # <- see tutorials for more options
7070
```
7171

7272
You can now populate the `Portfolio` class with your own custom hierarchy by taking inspiration from the [`demo.py`](https://github.com/MadeInPierre/finalynx/blob/main/examples/demo.py) example or by reading the [Getting Started](https://finalynx.readthedocs.io/en/latest/quickstart/getting_started.html) guide in the documentation and step-by-step [Tutorials](https://github.com/MadeInPierre/finalynx/tree/main/examples/tutorials). For additional details, checkout the full [API Reference](https://finalynx.readthedocs.io/en/latest/apidocs/index.html) or [ask a question](https://github.com/MadeInPierre/finalynx/discussions/new?category=q-a).
245 KB
Loading

finalynx/assistant.py

Lines changed: 7 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import json
22
import os
33
from datetime import date
4-
from typing import Any
5-
from typing import Dict
64
from typing import List
75
from typing import Optional
86
from typing import Tuple
@@ -13,19 +11,14 @@
1311
from finalynx import Dashboard
1412
from finalynx import Fetch
1513
from finalynx import Portfolio
16-
from finalynx.config import DEFAULT_CURRENCY
1714
from finalynx.config import get_active_theme as TH
1815
from finalynx.config import set_active_theme
16+
from finalynx.copilot.recommendations import render_recommendations
1917
from finalynx.fetch.source_base import SourceBase
2018
from finalynx.fetch.source_finary import SourceFinary
2119
from finalynx.portfolio.bucket import Bucket
2220
from finalynx.portfolio.envelope import Envelope
23-
from finalynx.portfolio.folder import Folder
24-
from finalynx.portfolio.folder import FolderDisplay
25-
from finalynx.portfolio.folder import SharedFolder
2621
from finalynx.portfolio.folder import Sidecar
27-
from finalynx.portfolio.node import Node
28-
from finalynx.portfolio.targets import Target
2922
from html2image import Html2Image
3023
from rich import inspect # noqa F401
3124
from rich import pretty
@@ -261,7 +254,7 @@ def render_panels(self) -> Columns:
261254
panels: List[ConsoleRenderable] = [
262255
Text(" "),
263256
Panel(
264-
self.render_recommendations(),
257+
render_recommendations(self.portfolio, self.envelopes),
265258
title="Recommendations",
266259
padding=(1, 2),
267260
expand=False,
@@ -288,80 +281,6 @@ def render_performance_report(self) -> Tree:
288281
tree.add(f"[{TH().TEXT}]Planned: [bold][{TH().ACCENT}]{perf_ideal:.1f} %[/] / year")
289282
return tree
290283

291-
def render_recommendations(self) -> Tree:
292-
"""Sort lines with non-zero deltas by envelopes and display them as a summary of transfers to make.
293-
Call either run() or initialize() first.
294-
"""
295-
dict_envs: Dict[str, Any] = {}
296-
297-
# Guide the user to set envelopes if not already done
298-
if not self.envelopes:
299-
return Tree(
300-
f"[dim {TH().TEXT}]"
301-
"To activate recommendations, set\n"
302-
"envelopes to your lines and give\n"
303-
"them to Assistant (tutorial #11)"
304-
)
305-
306-
# Find all folders with non-zero deltas and non-zero amounts (to avoid empty shared folders)
307-
def _get_folders(node: Folder) -> List[Folder]:
308-
found: List[Folder] = []
309-
for child in node.children:
310-
if isinstance(child, SharedFolder):
311-
if child.get_amount() > 0:
312-
found.append(child)
313-
elif isinstance(child, Folder):
314-
if child.display == FolderDisplay.EXPANDED:
315-
found += _get_folders(child)
316-
else:
317-
found.append(child)
318-
return found
319-
320-
# Check if a folder has non-zero deltas to be displayed in the recommendations
321-
def _check_node(node: Node) -> bool:
322-
return node.get_delta() != 0 and node.target.check() not in [
323-
Target.RESULT_NONE,
324-
Target.RESULT_OK,
325-
Target.RESULT_TOLERATED,
326-
]
327-
328-
# Render each envelope of folder's parent with a custom style along with the
329-
def _render_title(children: List[Any], name: str) -> Tuple[int, str]:
330-
total_delta = round(sum([c.get_delta() for c in children]))
331-
return total_delta, (
332-
f"[{TH().DELTA_POS if total_delta > 0 else TH().DELTA_NEG}]"
333-
f"{'+' if total_delta > 0 else ''}{total_delta} {DEFAULT_CURRENCY} "
334-
f"[{TH().FOLDER_COLOR} {TH().FOLDER_STYLE}]{name}[/]"
335-
)
336-
337-
# For each envelope, find all lines with non-zero deltas
338-
for envelope in self.envelopes:
339-
if lines := [line for line in envelope.lines if _check_node(line)]:
340-
delta, title = _render_title(lines, envelope.name)
341-
dict_envs[title] = (delta, lines)
342-
343-
# Render folders with non-zero deltas, classify them by parent name
344-
if folders := [f for f in _get_folders(self.portfolio) if _check_node(f)]:
345-
for parent_name in {f.parent.name for f in folders if f.parent}:
346-
items = [f for f in folders if f.parent and f.parent.name == parent_name]
347-
delta, title = _render_title(items, parent_name)
348-
dict_envs[title] = (delta, items)
349-
350-
# Render the tree with folders containing lines with non-zero deltas (sorted by delta)
351-
tree = Tree("Envelopes", hide_root=True, guide_style=TH().TREE_BRANCH)
352-
dict_sorted = {k: v[1] for k, v in sorted(dict_envs.items(), key=lambda item: item[1][0])} # type: ignore
353-
for i_env, envelope_name in enumerate(dict_sorted):
354-
node = tree.add(envelope_name)
355-
for i_line, line in enumerate(dict_sorted[envelope_name]):
356-
render = f"[{TH().TEXT}]{line._render_delta(children=dict_sorted[envelope_name])}{line._render_name()}" # type: ignore
357-
newline = bool(i_line == len(dict_sorted[envelope_name]) - 1 and i_env < len(dict_envs.keys()) - 1)
358-
node.add(render + ("\n" if newline else ""))
359-
360-
# If no envelopes are displayed, show a nice message instead
361-
if not tree.children:
362-
tree.add("You're on track! 🎉")
363-
return tree
364-
365284
def dashboard(self) -> None:
366285
"""Launch an interactive web dashboard! Call either run() or initialize() first."""
367286
console.log("Launching dashboard.")
@@ -421,9 +340,12 @@ def export_img(
421340
output_html = dashboard_console.export_html().replace("body {", f"body {{\n zoom: {zoom};")
422341

423342
# Convert the HTML to PNG
424-
Html2Image(output_path=full_path).screenshot(html_str=output_html, save_as=file_name, size=size)
343+
try:
344+
Html2Image(output_path=full_path).screenshot(html_str=output_html, save_as=file_name, size=size)
345+
console.print(f"Saved portfolio PNG to '{full_path + file_name}'")
346+
except Exception as e:
347+
console.log(f"[red][bold]Error:[/] Package html2image failed, skipping ({e})")
425348

426349
# Restore theme and return the image path
427350
set_active_theme(previous_theme)
428-
console.print(f"Saved portfolio PNG to '{full_path + file_name}'")
429351
return full_path + file_name
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from typing import Any
2+
from typing import Dict
3+
from typing import List
4+
from typing import Tuple
5+
6+
from finalynx.config import DEFAULT_CURRENCY
7+
from finalynx.config import get_active_theme as TH
8+
from finalynx.portfolio.envelope import Envelope
9+
from finalynx.portfolio.folder import Folder
10+
from finalynx.portfolio.folder import FolderDisplay
11+
from finalynx.portfolio.folder import Portfolio
12+
from finalynx.portfolio.folder import SharedFolder
13+
from finalynx.portfolio.node import Node
14+
from finalynx.portfolio.targets import Target
15+
from rich.tree import Tree
16+
17+
18+
# TODO Generate recommendations as a list of Transaction objects to make, then render them
19+
20+
21+
def render_recommendations(portfolio: Portfolio, envelopes: List[Envelope]) -> Tree:
22+
"""Sort lines with non-zero deltas by envelopes and display them as a summary of transfers to make.
23+
Call either run() or initialize() first.
24+
"""
25+
dict_envs: Dict[str, Any] = {}
26+
27+
# Guide the user to set envelopes if not already done
28+
if not envelopes:
29+
return Tree(
30+
f"[dim {TH().TEXT}]"
31+
"To activate recommendations, set\n"
32+
"envelopes to your lines and give\n"
33+
"them to Assistant (tutorial #11)"
34+
)
35+
36+
# Find all folders with non-zero deltas and non-zero amounts (to avoid empty shared folders)
37+
def _get_folders(node: Folder) -> List[Folder]:
38+
found: List[Folder] = []
39+
for child in node.children:
40+
if isinstance(child, SharedFolder):
41+
if child.get_amount() > 0:
42+
found.append(child)
43+
elif isinstance(child, Folder):
44+
if child.display == FolderDisplay.EXPANDED:
45+
found += _get_folders(child)
46+
else:
47+
found.append(child)
48+
return found
49+
50+
# Check if a folder has non-zero deltas to be displayed in the recommendations
51+
def _check_node(node: Node) -> bool:
52+
return node.get_delta() != 0 and node.target.check() not in [
53+
Target.RESULT_NONE,
54+
Target.RESULT_OK,
55+
Target.RESULT_TOLERATED,
56+
]
57+
58+
# Render each envelope of folder's parent with a custom style along with the
59+
def _render_title(children: List[Any], name: str) -> Tuple[int, str]:
60+
total_delta = round(sum([c.get_delta() for c in children]))
61+
return total_delta, (
62+
f"[{TH().DELTA_POS if total_delta > 0 else TH().DELTA_NEG}]"
63+
f"{'+' if total_delta > 0 else ''}{total_delta} {DEFAULT_CURRENCY} "
64+
f"[{TH().FOLDER_COLOR} {TH().FOLDER_STYLE}]{name}[/]"
65+
)
66+
67+
# For each envelope, find all lines with non-zero deltas
68+
for envelope in envelopes:
69+
if lines := [line for line in envelope.lines if _check_node(line)]:
70+
delta, title = _render_title(lines, envelope.name)
71+
dict_envs[title] = (delta, lines)
72+
73+
# Render folders with non-zero deltas, classify them by parent name
74+
if folders := [f for f in _get_folders(portfolio) if _check_node(f)]:
75+
for parent_name in {f.parent.name for f in folders if f.parent}:
76+
items = [f for f in folders if f.parent and f.parent.name == parent_name]
77+
delta, title = _render_title(items, parent_name)
78+
dict_envs[title] = (delta, items)
79+
80+
# Render the tree with folders containing lines with non-zero deltas (sorted by delta)
81+
tree = Tree("Envelopes", hide_root=True, guide_style=TH().TREE_BRANCH)
82+
dict_sorted = {k: v[1] for k, v in sorted(dict_envs.items(), key=lambda item: item[1][0])} # type: ignore
83+
for i_env, envelope_name in enumerate(dict_sorted):
84+
node = tree.add(envelope_name)
85+
for i_line, line in enumerate(dict_sorted[envelope_name]):
86+
render = f"[{TH().TEXT}]{line._render_delta(children=dict_sorted[envelope_name])}{line._render_name()}" # type: ignore
87+
newline = bool(i_line == len(dict_sorted[envelope_name]) - 1 and i_env < len(dict_envs.keys()) - 1)
88+
node.add(render + ("\n" if newline else ""))
89+
90+
# If no envelopes are displayed, show a nice message instead
91+
if not tree.children:
92+
tree.add("You're on track! 🎉")
93+
return tree

0 commit comments

Comments
 (0)