adds-up 0.1.0__py3-none-any.whl

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.
adds_up/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Reads a price list and reports the arithmetic in it that contradicts itself.
2
+
3
+ Everything here runs on values already printed in the document. No cost, no
4
+ margin, no benchmark, no opinion about whether a price is good.
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ __all__ = ["__version__"]
adds_up/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
adds_up/analyse.py ADDED
@@ -0,0 +1,91 @@
1
+ """Putting a run together: read the file, name the columns, run the checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from . import checks as check_module
8
+ from . import tables
9
+ from .columns import classify
10
+ from .models import Result, TableResult
11
+ from .numbers import convention_of
12
+
13
+
14
+ def _conventions(table: tables.Table, columns) -> tuple[dict, dict]:
15
+ """Decide, per column, how to read the separators in it.
16
+
17
+ Only columns a check might do arithmetic on are decided, because deciding
18
+ the convention of a description column is meaningless and its explanation
19
+ would be noise in the report.
20
+ """
21
+ numeric_roles = {
22
+ "quantity", "unit_price", "list_price", "net_price",
23
+ "discount_percent", "percent_of_list", "adjustment",
24
+ "minimum", "maximum",
25
+ }
26
+ conventions: dict[int, object] = {}
27
+ reasons: dict[int, str] = {}
28
+ for column in columns:
29
+ if column.role not in numeric_roles:
30
+ continue
31
+ convention, reason = convention_of(table.column(column.index))
32
+ conventions[column.index] = convention
33
+ reasons[column.index] = reason
34
+ return conventions, reasons
35
+
36
+
37
+ def analyse(
38
+ path: Path,
39
+ skip: set[str] | None = None,
40
+ sheet: str | None = None,
41
+ header_row: int | None = None,
42
+ overrides: dict[int, str] | None = None,
43
+ ) -> Result:
44
+ skip = skip or set()
45
+ result = Result(path=str(path))
46
+
47
+ def names_recognised(row: list[str]) -> int:
48
+ """How many of a candidate header row's labels this tool has a name for.
49
+
50
+ This is what settles a file with both a title block above the header
51
+ and a sub-header below it. It reads names, never contents, so it does
52
+ not weaken the rule that a column's role comes from what it is called.
53
+ """
54
+ return sum(1 for column in classify(row, overrides) if column.role)
55
+
56
+ for table in tables.read(
57
+ path, sheet=sheet, header_row=header_row, score=names_recognised
58
+ ):
59
+ columns = classify(table.header, overrides)
60
+ conventions, reasons = _conventions(table, columns)
61
+ table_result = TableResult(
62
+ sheet=table.name,
63
+ header_row_number=table.header_row_number,
64
+ header_reason=table.header_reason,
65
+ row_count=len(table.rows),
66
+ columns=columns,
67
+ conventions=reasons,
68
+ preamble=table.preamble,
69
+ header_width=len(table.header),
70
+ rows_wider_than_header=table.wider_than_header,
71
+ rows_narrower_than_header=table.narrower_than_header,
72
+ )
73
+ reader = check_module.Reader(
74
+ sheet=table.name,
75
+ header=table.header,
76
+ rows=table.rows,
77
+ columns=columns,
78
+ conventions=conventions,
79
+ header_row_number=table.header_row_number,
80
+ reasons=reasons,
81
+ row_numbers=table.row_numbers,
82
+ )
83
+ for name in check_module.CHECK_NAMES:
84
+ if name in skip:
85
+ table_result.checks.append(
86
+ check_module.CheckRun(name, False, "switched off with --skip")
87
+ )
88
+ continue
89
+ table_result.checks.append(check_module.RUNNERS[name](reader))
90
+ result.tables.append(table_result)
91
+ return result