Beispiel #1
0
def filepath_in_document_folder(
    documents_folder: str, account: str, filename: str, ledger: FavaLedger
) -> str:
    """File path for a document in the folder for an account.

    Args:
        documents_folder: The documents folder.
        account: The account to choose the subfolder for.
        filename: The filename of the document.
        ledger: The FavaLedger.

    Returns:
        The path that the document should be saved at.
    """

    if documents_folder not in ledger.options["documents"]:
        raise FavaAPIException(f"Not a documents folder: {documents_folder}.")

    if account not in ledger.attributes.accounts:
        raise FavaAPIException(f"Not a valid account: '{account}'")

    for sep in os.sep, os.altsep:
        if sep:
            filename = filename.replace(sep, " ")

    return path.normpath(
        path.join(
            path.dirname(ledger.beancount_file_path),
            documents_folder,
            *account.split(":"),
            filename,
        )
    )
Beispiel #2
0
def add_document():
    """Upload a document."""
    if not g.ledger.options["documents"]:
        raise FavaAPIException("You need to set a documents folder.")

    upload = request.files["file"]

    if not upload:
        raise FavaAPIException("No file uploaded.")

    filepath = filepath_in_document_folder(
        request.form["folder"],
        request.form["account"],
        upload.filename,
        g.ledger,
    )
    directory, filename = path.split(filepath)

    if path.exists(filepath):
        raise FavaAPIException(f"{filepath} already exists.")

    if not path.exists(directory):
        os.makedirs(directory, exist_ok=True)

    upload.save(filepath)

    if request.form.get("hash"):
        g.ledger.file.insert_metadata(
            request.form["hash"], "document", filename
        )
    return {"data": f"Uploaded to {filepath}"}
Beispiel #3
0
def delete_document(filename: str) -> str:
    """Delete a document."""
    if not is_document_or_import_file(filename, g.ledger):
        raise FavaAPIException("No valid document or import file.")

    if not path.exists(filename):
        raise FavaAPIException(f"{filename} does not exist.")

    remove(filename)
    return f"Deleted {filename}."
Beispiel #4
0
    def query_to_file(self, query_string: str, result_format: str):
        """Get query result as file.

        Arguments:
            query_string: A string, the query to run.
            result_format: The file format to save to.

        Returns:
            A tuple (name, data), where name is either 'query_result' or the
            name of a custom query if the query string is 'run name_of_query'.
            ``data`` contains the file contents.

        Raises:
            FavaAPIException: If the result format is not supported or the
            query failed.
        """
        name = "query_result"

        try:
            statement = self.parser.parse(query_string)
        except ParseError as exception:
            raise FavaAPIException(str(exception)) from exception

        if isinstance(statement, RunCustom):
            name = statement.query_name

            try:
                query = next(
                    query for query in self.queries if query.name == name
                )
            except StopIteration as exc:
                raise FavaAPIException(f'Query "{name}" not found.') from exc
            query_string = query.query_string

        try:
            types, rows = run_query(
                self.ledger.all_entries,
                self.ledger.options,
                query_string,
                numberify=True,
            )
        except (CompilationError, ParseError) as exception:
            raise FavaAPIException(str(exception)) from exception

        if result_format == "csv":
            data = to_csv(types, rows)
        else:
            if not HAVE_EXCEL:
                raise FavaAPIException("Result format not supported.")
            data = to_excel(types, rows, result_format, query_string)
        return name, data
Beispiel #5
0
def delete_document() -> str:
    """Delete a document."""
    filename = request.args.get("filename")
    if not filename:
        raise FavaAPIException("No filename specified.")

    if not is_document_or_import_file(filename, g.ledger):
        raise FavaAPIException("No valid document or import file.")

    if not path.exists(filename):
        raise FavaAPIException(f"{filename} does not exist.")

    remove(filename)
    return f"Deleted {filename}."
Beispiel #6
0
    def set_source(self, path: str, source: str, sha256sum: str) -> str:
        """Write to source file.

        Args:
            path: The path of the file.
            source: A string with the file contents.
            sha256sum: Hash of the file.

        Returns:
            The `sha256sum` of the updated file.

        Raises:
            FavaAPIException: If the file at `path` is not one of the
                source files or if the file was changed externally.
        """
        with self.lock:
            _, original_sha256sum = self.get_source(path)
            if original_sha256sum != sha256sum:
                raise FavaAPIException("The file changed externally.")

            contents = encode(source, encoding="utf-8")
            with open(path, "w+b") as file:
                file.write(contents)

            self.ledger.extensions.run_hook("after_write_source", path, source)
            self.ledger.load_file()

            return sha256(contents).hexdigest()
Beispiel #7
0
def save_entry_slice(entry: Directive, source_slice: str,
                     sha256sum: str) -> str:
    """Save slice of the source file for an entry.

    Args:
        entry: An entry.
        source_slice: The lines that the entry should be replaced with.
        sha256sum: The sha256sum of the current lines of the entry.

    Returns:
        The `sha256sum` of the new lines of the entry.

    Raises:
        FavaAPIException: If the file at `path` is not one of the
            source files.
    """

    with open(entry.meta["filename"], encoding="utf-8") as file:
        lines = file.readlines()

    first_entry_line = entry.meta["lineno"] - 1
    entry_lines = find_entry_lines(lines, first_entry_line)
    entry_source = "".join(entry_lines).rstrip("\n")
    if sha256_str(entry_source) != sha256sum:
        raise FavaAPIException("The file changed externally.")

    lines = (lines[:first_entry_line] + [source_slice + "\n"] +
             lines[first_entry_line + len(entry_lines):])
    with open(entry.meta["filename"], "w", encoding="utf-8") as file:
        file.writelines(lines)

    return sha256_str(source_slice)
Beispiel #8
0
def context() -> Any:
    """Entry context."""
    entry_hash = request.args.get("entry_hash")
    if not entry_hash:
        raise FavaAPIException("No entry hash given.")
    entry, balances, slice_, sha256sum = g.ledger.context(entry_hash)
    content = render_template("_context.html", entry=entry, balances=balances)
    return {"content": content, "sha256sum": sha256sum, "slice": slice_}
Beispiel #9
0
def get_move(account: str, new_name: str, filename: str) -> str:
    """Move a file."""
    if not g.ledger.options["documents"]:
        raise FavaAPIException("You need to set a documents folder.")

    new_path = filepath_in_document_folder(g.ledger.options["documents"][0],
                                           account, new_name, g.ledger)

    if not path.isfile(filename):
        raise FavaAPIException(f"Not a file: '{filename}'")
    if path.exists(new_path):
        raise FavaAPIException(f"Target file exists: '{new_path}'")

    if not path.exists(path.dirname(new_path)):
        os.makedirs(path.dirname(new_path), exist_ok=True)
    shutil.move(filename, new_path)

    return f"Moved {filename} to {new_path}."
Beispiel #10
0
def deserialise_posting(posting):
    """Parse JSON to a Beancount Posting."""
    amount = posting.get("amount", "")
    entries, errors, _ = parse_string(
        f'2000-01-01 * "" ""\n Assets:Account {amount}')
    if errors:
        raise FavaAPIException(f"Invalid amount: {amount}")
    pos = entries[0].postings[0]
    return pos._replace(account=posting["account"], meta=None)
Beispiel #11
0
def add_entries(request_data):
    """Add multiple entries."""
    try:
        entries = [deserialise(entry) for entry in request_data["entries"]]
    except KeyError as error:
        raise FavaAPIException(f"KeyError: {error}") from error

    g.ledger.file.insert_entries(entries)

    return f"Stored {len(entries)} entries."
Beispiel #12
0
def put_add_entries(entries: list[Any]) -> str:
    """Add multiple entries."""
    try:
        entries = [deserialise(entry) for entry in entries]
    except KeyError as error:
        raise FavaAPIException(f"KeyError: {error}") from error

    g.ledger.file.insert_entries(entries)

    return f"Stored {len(entries)} entries."
Beispiel #13
0
def get_query_result(query_string: str) -> Any:
    """Render a query result to HTML."""
    table = get_template_attribute("_query_table.html", "querytable")
    contents, types, rows = g.ledger.query_shell.execute_query(query_string)
    if contents:
        if "ERROR" in contents:
            raise FavaAPIException(contents)
    table = table(g.ledger, contents, types, rows)

    if types and g.ledger.charts.can_plot_query(types):
        return QueryResult(table, g.ledger.charts.query(types, rows))
    return QueryResult(table)
Beispiel #14
0
def move() -> str:
    """Move a file."""
    if not g.ledger.options["documents"]:
        raise FavaAPIException("You need to set a documents folder.")

    account = request.args.get("account")
    new_name = request.args.get("newName")
    filename = request.args.get("filename")

    if not account:
        raise FavaAPIException("No account specified.")

    if not filename:
        raise FavaAPIException("No filename specified.")

    if not new_name:
        raise FavaAPIException("No new filename given.")

    new_path = filepath_in_document_folder(
        g.ledger.options["documents"][0], account, new_name, g.ledger
    )

    if not path.isfile(filename):
        raise FavaAPIException(f"Not a file: '{filename}'")

    if path.exists(new_path):
        raise FavaAPIException(f"Target file exists: '{new_path}'")

    if not path.exists(path.dirname(new_path)):
        os.makedirs(path.dirname(new_path), exist_ok=True)

    shutil.move(filename, new_path)

    return f"Moved {filename} to {new_path}."
Beispiel #15
0
 def _wrapper() -> Response:
     if validator is not None:
         if method == "put":
             request_json = request.get_json(silent=True)
             if request_json is None:
                 raise FavaAPIException("Invalid JSON request.")
             data = request_json
         else:
             data = request.args
         res = func(*validator(data))
     else:
         res = func()
     return json_success(res)
Beispiel #16
0
    def statement_path(self, entry_hash: str, metadata_key: str) -> str:
        """Returns the path for a statement found in the specified entry."""
        entry = self.get_entry(entry_hash)
        value = entry.meta[metadata_key]

        accounts = set(get_entry_accounts(entry))
        full_path = join(dirname(entry.meta["filename"]), value)
        for document in self.all_entries_by_type.Document:
            if document.filename == full_path:
                return document.filename
            if document.account in accounts:
                if basename(document.filename) == value:
                    return document.filename

        raise FavaAPIException("Statement not found.")
Beispiel #17
0
def deserialise(json_entry: Any) -> Directive:
    """Parse JSON to a Beancount entry.

    Args:
        json_entry: The entry.

    Raises:
        KeyError: if one of the required entry fields is missing.
        FavaAPIException: if the type of the given entry is not supported.
    """
    date = parse_date(json_entry.get("date", ""))[0]
    if not isinstance(date, datetime.date):
        raise FavaAPIException("Invalid entry date.")
    if json_entry["type"] == "Transaction":
        narration, tags, links = extract_tags_links(json_entry["narration"])
        postings = [deserialise_posting(pos) for pos in json_entry["postings"]]
        return Transaction(
            json_entry["meta"],
            date,
            json_entry.get("flag", ""),
            json_entry.get("payee", ""),
            narration or "",
            tags,
            links,
            postings,
        )
    if json_entry["type"] == "Balance":
        raw_amount = json_entry["amount"]
        amount = Amount(D(str(raw_amount["number"])), raw_amount["currency"])

        return Balance(json_entry["meta"], date, json_entry["account"], amount,
                       None, None)
    if json_entry["type"] == "Note":
        comment = json_entry["comment"].replace('"', "")
        return Note(json_entry["meta"], date, json_entry["account"], comment)
    raise FavaAPIException("Unsupported entry type.")
Beispiel #18
0
    def query(self, types, rows):
        """Chart for a query.

        Args:
            types: The list of result row types.
            rows: The result rows.
        """

        if not self.can_plot_query(types):
            raise FavaAPIException("Can not plot the given chart.")
        if types[0][1] is date:
            return [
                {"date": date, "balance": units(inv)} for date, inv in rows
            ]
        return [{"group": group, "balance": units(inv)} for group, inv in rows]
Beispiel #19
0
def query_result():
    """Render a query result to HTML."""
    query = request.args.get("query_string", "")
    table = get_template_attribute("_query_table.html", "querytable")
    contents, types, rows = g.ledger.query_shell.execute_query(query)
    if contents:
        if "ERROR" in contents:
            raise FavaAPIException(contents)
    table = table(contents, types, rows)
    if types and g.ledger.charts.can_plot_query(types):
        return {
            "chart": g.ledger.charts.query(types, rows),
            "table": table,
        }
    return {"table": table}
Beispiel #20
0
    def get_entry(self, entry_hash: str) -> Directive:
        """Find an entry.

        Arguments:
            entry_hash: Hash of the entry.

        Returns:
            The entry with the given hash.
        Raises:
            FavaAPIException: If there is no entry for the given hash.
        """
        try:
            return next(entry for entry in self.all_entries
                        if entry_hash == hash_entry(entry))
        except StopIteration:
            raise FavaAPIException(f'No entry found for hash "{entry_hash}"')
Beispiel #21
0
    def statement_path(self, entry_hash: str, metadata_key: str) -> str:
        """Returns the path for a statement found in the specified entry."""
        entry = self.get_entry(entry_hash)
        value = entry.meta[metadata_key]

        paths = [os.path.join(os.path.dirname(entry.meta["filename"]), value)]
        paths.extend([
            self.join_path(document_root, *posting.account.split(":"), value)
            for posting in getattr(entry, "postings", [])
            for document_root in self.options["documents"]
        ])

        for path in paths:
            if os.path.isfile(path):
                return path

        raise FavaAPIException("Statement not found.")
Beispiel #22
0
    def portfolio_accounts(self):
        """An account tree based on matching regex patterns."""
        tree = self.ledger.root_tree
        portfolios = []

        for option in self.config:
            opt_key = option[0]
            if opt_key == "account_name_pattern":
                portfolio = self._account_name_pattern(tree, option[1])
            elif opt_key == "account_open_metadata_pattern":
                portfolio = self._account_metadata_pattern(
                    tree, option[1][0], option[1][1])
            else:
                raise FavaAPIException("Portfolio List: Invalid option.")
            portfolios.append(portfolio)

        return portfolios
    def portfolio_accounts(self, begin=None, end=None):
        """An account tree based on matching regex patterns."""
        portfolios = []

        try:
            self.load_report()

            if begin:
                tree = Tree(iter_entry_dates(self.ledger.entries, begin, end))
            else:
                tree = self.ledger.root_tree

            for option in self.config:
                opt_key = option[0]
                if opt_key == "account_name_pattern":
                    portfolio = self._account_name_pattern(tree, end, option[1])
                elif opt_key == "account_open_metadata_pattern":
                    portfolio = self._account_metadata_pattern(
                        tree, end, option[1][0], option[1][1]
                    )
                else:
                    exception = FavaAPIException("Classy Portfolio: Invalid option.")
                    raise (exception)

                portfolio = (
                    portfolio[0],  # title
                    portfolio[1],  # subtitle
                    (
                        insert_rowspans(portfolio[2][0], portfolio[2][1], True),
                        portfolio[2][1],
                    ),  # portfolio data
                )
                portfolios.append(portfolio)

        except Exception as exc:
            traceback.print_exc(file=sys.stdout)

        return portfolios
Beispiel #24
0
    def get_source(self, path: str) -> Tuple[str, str]:
        """Get source files.

        Args:
            path: The path of the file.

        Returns:
            A string with the file contents and the `sha256sum` of the file.

        Raises:
            FavaAPIException: If the file at `path` is not one of the
                source files.
        """
        if path not in self.ledger.options["include"]:
            raise FavaAPIException("Trying to read a non-source file")

        with open(path, mode="rb") as file:
            contents = file.read()

        sha256sum = sha256(contents).hexdigest()
        source = decode(contents)

        return source, sha256sum
Beispiel #25
0
 def _wrapper() -> Any:
     request_data = request.get_json()
     if request_data is None:
         raise FavaAPIException("Invalid JSON request.")
     return json_success(func(request_data))
Beispiel #26
0
def test_apiexception() -> None:
    with pytest.raises(FavaAPIException) as exception:
        raise FavaAPIException("error")
    assert str(exception.value) == "error"
Beispiel #27
0
 def _wrapper(*args, **kwargs):
     request_data = request.get_json()
     if request_data is None:
         raise FavaAPIException("Invalid JSON request.")
     res = func(request_data, *args, **kwargs)
     return jsonify({"success": True, "data": res})