Source code for finalynx.copilot.recommendations

from typing import Any
from typing import Dict
from typing import List
from typing import Tuple

from finalynx.config import DEFAULT_CURRENCY
from finalynx.config import get_active_theme as TH
from finalynx.portfolio.envelope import Envelope
from finalynx.portfolio.folder import Folder
from finalynx.portfolio.folder import FolderDisplay
from finalynx.portfolio.folder import Portfolio
from finalynx.portfolio.folder import SharedFolder
from finalynx.portfolio.node import Node
from finalynx.portfolio.targets import Target
from rich.tree import Tree


# TODO Generate recommendations as a list of Transaction objects to make, then render them


[docs]def render_recommendations(portfolio: Portfolio, envelopes: List[Envelope]) -> Tree: """Sort lines with non-zero deltas by envelopes and display them as a summary of transfers to make. Call either run() or initialize() first. """ dict_envs: Dict[str, Any] = {} # Guide the user to set envelopes if not already done if not envelopes: return Tree( f"[dim {TH().TEXT}]" "To activate recommendations, set\n" "envelopes to your lines and give\n" "them to Assistant (tutorial #11)" ) # Find all folders with non-zero deltas and non-zero amounts (to avoid empty shared folders) def _get_folders(node: Folder) -> List[Folder]: found: List[Folder] = [] for child in node.children: if isinstance(child, SharedFolder): if child.get_amount() > 0: found.append(child) elif isinstance(child, Folder): if child.display == FolderDisplay.EXPANDED: found += _get_folders(child) else: found.append(child) return found # Check if a folder has non-zero deltas to be displayed in the recommendations def _check_node(node: Node) -> bool: return node.get_delta() != 0 and node.target.check() not in [ Target.RESULT_NONE, Target.RESULT_OK, Target.RESULT_TOLERATED, ] # Render each envelope of folder's parent with a custom style along with the def _render_title(children: List[Any], name: str) -> Tuple[int, str]: total_delta = round(sum([c.get_delta() for c in children])) return total_delta, ( f"[{TH().DELTA_POS if total_delta > 0 else TH().DELTA_NEG}]" f"{'+' if total_delta > 0 else ''}{total_delta} {DEFAULT_CURRENCY} " f"[{TH().FOLDER_COLOR} {TH().FOLDER_STYLE}]{name}[/]" ) # For each envelope, find all lines with non-zero deltas for envelope in envelopes: if lines := [line for line in envelope.lines if _check_node(line)]: delta, title = _render_title(lines, envelope.name) dict_envs[title] = (delta, lines) # Render folders with non-zero deltas, classify them by parent name if folders := [f for f in _get_folders(portfolio) if _check_node(f)]: for parent_name in {f.parent.name for f in folders if f.parent}: items = [f for f in folders if f.parent and f.parent.name == parent_name] delta, title = _render_title(items, parent_name) dict_envs[title] = (delta, items) # Render the tree with folders containing lines with non-zero deltas (sorted by delta) tree = Tree("Envelopes", hide_root=True, guide_style=TH().TREE_BRANCH) dict_sorted = {k: v[1] for k, v in sorted(dict_envs.items(), key=lambda item: item[1][0])} # type: ignore for i_env, envelope_name in enumerate(dict_sorted): node = tree.add(envelope_name) for i_line, line in enumerate(dict_sorted[envelope_name]): render = f"[{TH().TEXT}]{line._render_delta(children=dict_sorted[envelope_name])}{line._render_name()}" # type: ignore newline = bool(i_line == len(dict_sorted[envelope_name]) - 1 and i_env < len(dict_envs.keys()) - 1) node.add(render + ("\n" if newline else "")) # If no envelopes are displayed, show a nice message instead if not tree.children: tree.add("You're on track! 🎉") return tree