indico_toolkit 7.2.1__tar.gz → 7.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.
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/PKG-INFO +1 -1
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/__init__.py +1 -1
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/etloutput/__init__.py +11 -10
- {indico_toolkit-7.2.1/indico_toolkit/results/predictions → indico_toolkit-7.2.2/indico_toolkit/etloutput}/box.py +27 -6
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/etloutput/cell.py +30 -3
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/etloutput/etloutput.py +60 -32
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/etloutput/range.py +19 -1
- {indico_toolkit-7.2.1/indico_toolkit/results/predictions → indico_toolkit-7.2.2/indico_toolkit/etloutput}/span.py +23 -6
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/etloutput/table.py +41 -2
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/etloutput/token.py +18 -2
- indico_toolkit-7.2.2/indico_toolkit/etloutput/utils.py +69 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/__init__.py +1 -6
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/normalization.py +1 -4
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/predictionlist.py +43 -6
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/predictions/__init__.py +1 -8
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/predictions/citation.py +3 -6
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/predictions/classification.py +2 -4
- indico_toolkit-7.2.2/indico_toolkit/results/predictions/documentextraction.py +204 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/predictions/formextraction.py +3 -5
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/predictions/group.py +1 -4
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/predictions/prediction.py +12 -5
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/predictions/summarization.py +21 -14
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/predictions/unbundling.py +14 -5
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/result.py +1 -2
- indico_toolkit-7.2.2/indico_toolkit/results/utils.py +42 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/pyproject.toml +1 -1
- indico_toolkit-7.2.1/indico_toolkit/etloutput/errors.py +0 -16
- indico_toolkit-7.2.1/indico_toolkit/results/errors.py +0 -4
- indico_toolkit-7.2.1/indico_toolkit/results/predictions/documentextraction.py +0 -100
- indico_toolkit-7.2.1/indico_toolkit/results/utils.py +0 -101
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/LICENSE +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/README.md +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/association/__init__.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/association/association.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/association/extracted_tokens.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/association/line_items.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/association/positioning.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/association/splitting.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/auto_review/__init__.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/auto_review/auto_review_functions.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/auto_review/auto_reviewer.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/client.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/errors.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/indico_wrapper/__init__.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/indico_wrapper/download.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/indico_wrapper/indico_wrapper.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/indico_wrapper/modelop.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/indico_wrapper/workflow.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/metrics/__init__.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/metrics/compare_ground_truth.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/metrics/compare_models.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/metrics/metrics.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/metrics/plotting.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/ocr/__init__.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/ocr/ondoc_object.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/ocr/standard_object.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/pipelines/__init__.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/pipelines/file_processing.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/polling/__init__.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/polling/autoreview.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/polling/downstream.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/polling/queries.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/py.typed +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/document.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/predictions/extraction.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/review.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/results/task.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/retry.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/snapshots/__init__.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/snapshots/snapshot.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/structure/__init__.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/structure/create_structure.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/structure/queries.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/structure/utils.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/types/__init__.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/types/classification.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/types/extractions.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/types/predictions.py +0 -0
- {indico_toolkit-7.2.1 → indico_toolkit-7.2.2}/indico_toolkit/types/workflow_object.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING, TypeAlias, TypeVar
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
from .cell import Cell, CellType
|
|
6
|
-
from .errors import EtlOutputError, TableCellNotFoundError, TokenNotFoundError
|
|
3
|
+
from .box import NULL_BOX, Box
|
|
4
|
+
from .cell import NULL_CELL, Cell, CellType
|
|
7
5
|
from .etloutput import EtlOutput
|
|
8
|
-
from .range import Range
|
|
9
|
-
from .
|
|
10
|
-
from .
|
|
6
|
+
from .range import NULL_RANGE, Range
|
|
7
|
+
from .span import NULL_SPAN, Span
|
|
8
|
+
from .table import NULL_TABLE, Table
|
|
9
|
+
from .token import NULL_TOKEN, Token
|
|
10
|
+
from .utils import get, has, json_loaded, str_decoded
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from collections.abc import Awaitable, Callable
|
|
@@ -17,17 +17,18 @@ __all__ = (
|
|
|
17
17
|
"Cell",
|
|
18
18
|
"CellType",
|
|
19
19
|
"EtlOutput",
|
|
20
|
-
"EtlOutputError",
|
|
21
20
|
"load",
|
|
22
21
|
"load_async",
|
|
23
22
|
"NULL_BOX",
|
|
23
|
+
"NULL_CELL",
|
|
24
|
+
"NULL_RANGE",
|
|
24
25
|
"NULL_SPAN",
|
|
26
|
+
"NULL_TABLE",
|
|
27
|
+
"NULL_TOKEN",
|
|
25
28
|
"Range",
|
|
26
29
|
"Span",
|
|
27
30
|
"Table",
|
|
28
|
-
"TableCellNotFoundError",
|
|
29
31
|
"Token",
|
|
30
|
-
"TokenNotFoundError",
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
Loadable: TypeAlias = "dict[str, object] | list[object] | str | bytes"
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Final
|
|
3
3
|
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
if TYPE_CHECKING:
|
|
7
|
-
from typing import Final
|
|
4
|
+
from .utils import get
|
|
8
5
|
|
|
9
6
|
|
|
10
7
|
@dataclass(frozen=True)
|
|
@@ -18,6 +15,30 @@ class Box:
|
|
|
18
15
|
def __bool__(self) -> bool:
|
|
19
16
|
return self != NULL_BOX
|
|
20
17
|
|
|
18
|
+
def __and__(self, other: "Box") -> "Box":
|
|
19
|
+
"""
|
|
20
|
+
Return a new `Box` for the overlap between `self` and `other`
|
|
21
|
+
or `NULL_BOX` if they don't overlap.
|
|
22
|
+
|
|
23
|
+
Supports set-like `extraction.box & cell.box` syntax.
|
|
24
|
+
"""
|
|
25
|
+
if (
|
|
26
|
+
self.page != other.page
|
|
27
|
+
or self.bottom <= other.top # `self` is above `other`
|
|
28
|
+
or self.top >= other.bottom # `self` is below `other`
|
|
29
|
+
or self.right <= other.left # `self` is to the left of `other`
|
|
30
|
+
or self.left >= other.right # `self` is to the right of `other`
|
|
31
|
+
):
|
|
32
|
+
return NULL_BOX
|
|
33
|
+
else:
|
|
34
|
+
return Box(
|
|
35
|
+
page=self.page,
|
|
36
|
+
top=max(self.top, other.top),
|
|
37
|
+
left=max(self.left, other.left),
|
|
38
|
+
right=min(self.right, other.right),
|
|
39
|
+
bottom=min(self.bottom, other.bottom),
|
|
40
|
+
)
|
|
41
|
+
|
|
21
42
|
def __lt__(self, other: "Box") -> bool:
|
|
22
43
|
"""
|
|
23
44
|
Bounding boxes are sorted with vertical hysteresis. Those on the same line are
|
|
@@ -58,4 +79,4 @@ class Box:
|
|
|
58
79
|
# object rather than using `None` or raising an error. This lets you e.g. sort by the
|
|
59
80
|
# `box` attribute without having to constantly check for `None`, while still allowing
|
|
60
81
|
# you do a "None check" with `bool(extraction.box)` or `extraction.box == NULL_BOX`.
|
|
61
|
-
NULL_BOX:
|
|
82
|
+
NULL_BOX: Final = Box(page=0, top=0, left=0, right=0, bottom=0)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from enum import Enum
|
|
3
|
+
from typing import Final
|
|
3
4
|
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
from .
|
|
5
|
+
from .box import NULL_BOX, Box
|
|
6
|
+
from .range import NULL_RANGE, Range
|
|
7
|
+
from .span import NULL_SPAN, Span
|
|
8
|
+
from .utils import get
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class CellType(Enum):
|
|
@@ -19,6 +21,18 @@ class Cell:
|
|
|
19
21
|
range: Range
|
|
20
22
|
spans: "tuple[Span, ...]"
|
|
21
23
|
|
|
24
|
+
def __bool__(self) -> bool:
|
|
25
|
+
return self != NULL_CELL
|
|
26
|
+
|
|
27
|
+
def __hash__(self) -> int:
|
|
28
|
+
"""
|
|
29
|
+
Uniquely identify cells by hashing their bounding box and spans.
|
|
30
|
+
|
|
31
|
+
This is small speedup for `.groupby(attrgetter("cell"))` compared to
|
|
32
|
+
dataclasses's default __hash__ implementation.
|
|
33
|
+
"""
|
|
34
|
+
return hash((self.box, self.spans))
|
|
35
|
+
|
|
22
36
|
@property
|
|
23
37
|
def span(self) -> Span:
|
|
24
38
|
"""
|
|
@@ -45,3 +59,16 @@ class Cell:
|
|
|
45
59
|
range=Range.from_dict(cell),
|
|
46
60
|
spans=tuple(map(Span.from_dict, get(cell, list, "doc_offsets"))),
|
|
47
61
|
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# It's more ergonomic to represent the lack of cells with a special null cell object
|
|
65
|
+
# rather than using `None` or raising an error. This lets you e.g. sort by the `cell`
|
|
66
|
+
# attribute without having to constantly check for `None`, while still allowing you do
|
|
67
|
+
# a "None check" with `bool(extraction.cell)` or `extraction.cell == NULL_CELL`.
|
|
68
|
+
NULL_CELL: Final = Cell(
|
|
69
|
+
type=CellType.CONTENT,
|
|
70
|
+
text="",
|
|
71
|
+
box=NULL_BOX,
|
|
72
|
+
range=NULL_RANGE,
|
|
73
|
+
spans=tuple(),
|
|
74
|
+
)
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import itertools
|
|
2
2
|
from bisect import bisect_left, bisect_right
|
|
3
|
+
from collections import namedtuple
|
|
3
4
|
from dataclasses import dataclass
|
|
5
|
+
from functools import cached_property
|
|
4
6
|
from operator import attrgetter
|
|
5
7
|
from typing import TYPE_CHECKING
|
|
6
8
|
|
|
7
|
-
from
|
|
8
|
-
from .errors import TableCellNotFoundError, TokenNotFoundError
|
|
9
|
+
from .box import Box
|
|
9
10
|
from .table import Table
|
|
10
|
-
from .token import Token
|
|
11
|
+
from .token import NULL_TOKEN, Token
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
13
|
-
from collections.abc import Iterable
|
|
14
|
+
from collections.abc import Iterable, Iterator
|
|
14
15
|
|
|
15
16
|
from .cell import Cell
|
|
17
|
+
from .span import Span
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
@dataclass(frozen=True)
|
|
@@ -54,18 +56,19 @@ class EtlOutput:
|
|
|
54
56
|
tables_on_page=table_pages,
|
|
55
57
|
)
|
|
56
58
|
|
|
57
|
-
def token_for(self, span: Span) -> Token:
|
|
59
|
+
def token_for(self, span: "Span") -> Token:
|
|
58
60
|
"""
|
|
59
|
-
Return a `Token` that contains every character from `span
|
|
60
|
-
|
|
61
|
+
Return a `Token` that contains every character from `span`
|
|
62
|
+
or `NULL_TOKEN` if one doesn't exist.
|
|
61
63
|
"""
|
|
62
64
|
try:
|
|
63
65
|
tokens = self.tokens_on_page[span.page]
|
|
64
66
|
first = bisect_right(tokens, span.start, key=attrgetter("span.end"))
|
|
65
67
|
last = bisect_left(tokens, span.end, lo=first, key=attrgetter("span.start"))
|
|
66
68
|
tokens = tokens[first:last]
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
assert tokens
|
|
70
|
+
except (AssertionError, IndexError, ValueError):
|
|
71
|
+
return NULL_TOKEN
|
|
69
72
|
|
|
70
73
|
return Token(
|
|
71
74
|
text=self.text[span.slice],
|
|
@@ -79,28 +82,53 @@ class EtlOutput:
|
|
|
79
82
|
span=span,
|
|
80
83
|
)
|
|
81
84
|
|
|
82
|
-
|
|
85
|
+
_TableCellSpan = namedtuple("_TableCellSpan", ["table", "cell", "span"])
|
|
86
|
+
|
|
87
|
+
@cached_property
|
|
88
|
+
def _table_cell_spans_on_page(self) -> "tuple[tuple[_TableCellSpan, ...], ...]":
|
|
89
|
+
"""
|
|
90
|
+
Order table cells on each page by their spans such that they can be bisected.
|
|
91
|
+
"""
|
|
92
|
+
return tuple(
|
|
93
|
+
tuple(
|
|
94
|
+
sorted(
|
|
95
|
+
(
|
|
96
|
+
self._TableCellSpan(table, cell, span)
|
|
97
|
+
for table in page_tables
|
|
98
|
+
for cell in table.cells
|
|
99
|
+
for span in cell.spans
|
|
100
|
+
if span
|
|
101
|
+
),
|
|
102
|
+
key=attrgetter("span"),
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
for page_tables in self.tables_on_page
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def table_cells_for(self, span: "Span") -> "Iterator[tuple[Table, Cell]]":
|
|
83
109
|
"""
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
Yield the table cells that overlap with `span`.
|
|
111
|
+
|
|
112
|
+
Note: a single span may overlap the same cell multiple times causing it to be
|
|
113
|
+
yielded multiple times. Deduplication in `DocumentExtraction.table_cells`
|
|
114
|
+
accounts for this when OCR is assigned with `PredictionList.assign_ocr()`.
|
|
86
115
|
"""
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
raise TableCellNotFoundError(f"no cell contains {token!r}")
|
|
116
|
+
try:
|
|
117
|
+
page_table_cell_spans = self._table_cell_spans_on_page[span.page]
|
|
118
|
+
first = bisect_right(
|
|
119
|
+
page_table_cell_spans,
|
|
120
|
+
span.start,
|
|
121
|
+
key=attrgetter("span.end"),
|
|
122
|
+
)
|
|
123
|
+
last = bisect_left(
|
|
124
|
+
page_table_cell_spans,
|
|
125
|
+
span.end,
|
|
126
|
+
lo=first,
|
|
127
|
+
key=attrgetter("span.start"),
|
|
128
|
+
)
|
|
129
|
+
table_cell_spans = page_table_cell_spans[first:last]
|
|
130
|
+
except (IndexError, ValueError):
|
|
131
|
+
table_cell_spans = tuple()
|
|
132
|
+
|
|
133
|
+
for table, cell, span in table_cell_spans:
|
|
134
|
+
yield table, cell
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
+
from typing import Final
|
|
2
3
|
|
|
3
|
-
from
|
|
4
|
+
from .utils import get
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
@dataclass(order=True, frozen=True)
|
|
@@ -12,6 +13,9 @@ class Range:
|
|
|
12
13
|
rows: "tuple[int, ...]"
|
|
13
14
|
columns: "tuple[int, ...]"
|
|
14
15
|
|
|
16
|
+
def __bool__(self) -> bool:
|
|
17
|
+
return self != NULL_RANGE
|
|
18
|
+
|
|
15
19
|
@staticmethod
|
|
16
20
|
def from_dict(cell: object) -> "Range":
|
|
17
21
|
"""
|
|
@@ -28,3 +32,17 @@ class Range:
|
|
|
28
32
|
rows=tuple(rows),
|
|
29
33
|
columns=tuple(columns),
|
|
30
34
|
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# It's more ergonomic to represent the lack of ranges with a special null range object
|
|
38
|
+
# rather than using `None` or raising an error. This lets you e.g. sort by the `range`
|
|
39
|
+
# attribute without having to constantly check for `None`, while still allowing you do
|
|
40
|
+
# a "None check" with `bool(cell.range)` or `cell.range == NULL_RANGE`.
|
|
41
|
+
NULL_RANGE: Final = Range(
|
|
42
|
+
row=0,
|
|
43
|
+
column=0,
|
|
44
|
+
rowspan=0,
|
|
45
|
+
columnspan=0,
|
|
46
|
+
rows=tuple(),
|
|
47
|
+
columns=tuple(),
|
|
48
|
+
)
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Any, Final
|
|
3
3
|
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
if TYPE_CHECKING:
|
|
7
|
-
from typing import Any, Final
|
|
4
|
+
from .utils import get
|
|
8
5
|
|
|
9
6
|
|
|
10
7
|
@dataclass(order=True, frozen=True)
|
|
@@ -20,6 +17,26 @@ class Span:
|
|
|
20
17
|
def __bool__(self) -> bool:
|
|
21
18
|
return self != NULL_SPAN
|
|
22
19
|
|
|
20
|
+
def __and__(self, other: "Span") -> "Span":
|
|
21
|
+
"""
|
|
22
|
+
Return a new `Span` for the overlap between `self` and `other`
|
|
23
|
+
or `NULL_SPAN` if they don't overlap.
|
|
24
|
+
|
|
25
|
+
Supports set-like `extraction.span & cell.span` syntax.
|
|
26
|
+
"""
|
|
27
|
+
if (
|
|
28
|
+
self.page != other.page
|
|
29
|
+
or self.end <= other.start # `self` is to the left of `other`
|
|
30
|
+
or self.start >= other.end # `self` is to the right of `other`
|
|
31
|
+
):
|
|
32
|
+
return NULL_SPAN
|
|
33
|
+
else:
|
|
34
|
+
return Span(
|
|
35
|
+
page=self.page,
|
|
36
|
+
start=max(self.start, other.start),
|
|
37
|
+
end=min(self.end, other.end),
|
|
38
|
+
)
|
|
39
|
+
|
|
23
40
|
@staticmethod
|
|
24
41
|
def from_dict(span: object) -> "Span":
|
|
25
42
|
return Span(
|
|
@@ -40,4 +57,4 @@ class Span:
|
|
|
40
57
|
# rather than using `None` or raising an error. This lets you e.g. sort by the `span`
|
|
41
58
|
# attribute without having to constantly check for `None`, while still allowing you do
|
|
42
59
|
# a "None check" with `bool(extraction.span)` or `extraction.span == NULL_SPAN`.
|
|
43
|
-
NULL_SPAN:
|
|
60
|
+
NULL_SPAN: Final = Span(page=0, start=0, end=0)
|
|
@@ -1,18 +1,40 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from operator import attrgetter
|
|
3
|
+
from typing import Final
|
|
3
4
|
|
|
4
|
-
from
|
|
5
|
-
from ..results.utils import get
|
|
5
|
+
from .box import NULL_BOX, Box
|
|
6
6
|
from .cell import Cell
|
|
7
|
+
from .span import NULL_SPAN, Span
|
|
8
|
+
from .utils import get
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
@dataclass(frozen=True)
|
|
10
12
|
class Table:
|
|
11
13
|
box: Box
|
|
14
|
+
spans: "tuple[Span, ...]"
|
|
12
15
|
cells: "tuple[Cell, ...]"
|
|
13
16
|
rows: "tuple[tuple[Cell, ...], ...]"
|
|
14
17
|
columns: "tuple[tuple[Cell, ...], ...]"
|
|
15
18
|
|
|
19
|
+
def __bool__(self) -> bool:
|
|
20
|
+
return self != NULL_TABLE
|
|
21
|
+
|
|
22
|
+
def __hash__(self) -> int:
|
|
23
|
+
"""
|
|
24
|
+
Uniquely identify tables by hashing their bounding box and spans.
|
|
25
|
+
|
|
26
|
+
This is an order of magnitude speedup for `.groupby(attrgetter("table"))`
|
|
27
|
+
compared to dataclasses's default __hash__ implementation.
|
|
28
|
+
"""
|
|
29
|
+
return hash((self.box, self.spans))
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def span(self) -> Span:
|
|
33
|
+
"""
|
|
34
|
+
Return the first `Span` the table covers or `NULL_SPAN` otherwise.
|
|
35
|
+
"""
|
|
36
|
+
return self.spans[0] if self.spans else NULL_SPAN
|
|
37
|
+
|
|
16
38
|
@staticmethod
|
|
17
39
|
def from_dict(table: object) -> "Table":
|
|
18
40
|
"""
|
|
@@ -50,9 +72,26 @@ class Table:
|
|
|
50
72
|
for column in range(column_count)
|
|
51
73
|
) # fmt: skip
|
|
52
74
|
|
|
75
|
+
for doc_offset in get(table, list, "doc_offsets"):
|
|
76
|
+
doc_offset["page_num"] = page
|
|
77
|
+
|
|
53
78
|
return Table(
|
|
54
79
|
box=Box.from_dict(get(table, dict, "position")),
|
|
80
|
+
spans=tuple(map(Span.from_dict, get(table, list, "doc_offsets"))),
|
|
55
81
|
cells=cells,
|
|
56
82
|
rows=rows,
|
|
57
83
|
columns=columns,
|
|
58
84
|
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# It's more ergonomic to represent the lack of tables with a special null table object
|
|
88
|
+
# rather than using `None` or raising an error. This lets you e.g. group by the `table`
|
|
89
|
+
# attribute without having to constantly check for `None`, while still allowing you do
|
|
90
|
+
# a "None check" with `bool(extraction.table)` or `extraction.table == NULL_TABLE`.
|
|
91
|
+
NULL_TABLE: Final = Table(
|
|
92
|
+
box=NULL_BOX,
|
|
93
|
+
spans=tuple(),
|
|
94
|
+
cells=tuple(),
|
|
95
|
+
rows=tuple(),
|
|
96
|
+
columns=tuple(),
|
|
97
|
+
)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
+
from typing import Final
|
|
2
3
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
4
|
+
from .box import NULL_BOX, Box
|
|
5
|
+
from .span import NULL_SPAN, Span
|
|
6
|
+
from .utils import get
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
@dataclass(frozen=True)
|
|
@@ -10,6 +12,9 @@ class Token:
|
|
|
10
12
|
box: Box
|
|
11
13
|
span: Span
|
|
12
14
|
|
|
15
|
+
def __bool__(self) -> bool:
|
|
16
|
+
return self != NULL_TOKEN
|
|
17
|
+
|
|
13
18
|
@staticmethod
|
|
14
19
|
def from_dict(token: object) -> "Token":
|
|
15
20
|
"""
|
|
@@ -23,3 +28,14 @@ class Token:
|
|
|
23
28
|
box=Box.from_dict(get(token, dict, "position")),
|
|
24
29
|
span=Span.from_dict(get(token, dict, "doc_offset")),
|
|
25
30
|
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# It's more ergonomic to represent the lack of tokens with a special null token object
|
|
34
|
+
# rather than using `None` or raising an error. This lets you e.g. sort by the `token`
|
|
35
|
+
# attribute without having to constantly check for `None`, while still allowing you do
|
|
36
|
+
# a "None check" with `bool(extraction.token)` or `extraction.token == NULL_TOKEN`.
|
|
37
|
+
NULL_TOKEN: Final = Token(
|
|
38
|
+
text="",
|
|
39
|
+
box=NULL_BOX,
|
|
40
|
+
span=NULL_SPAN,
|
|
41
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, TypeVar
|
|
3
|
+
|
|
4
|
+
Value = TypeVar("Value")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get(value: object, value_type: "type[Value]", *keys: "str | int") -> Value:
|
|
8
|
+
"""
|
|
9
|
+
Return the value of type `value_type` obtained by traversing `value` using `keys`.
|
|
10
|
+
Raise an error if a key doesn't exist or the value has the wrong type.
|
|
11
|
+
"""
|
|
12
|
+
for key in keys:
|
|
13
|
+
if isinstance(value, dict):
|
|
14
|
+
if key in value:
|
|
15
|
+
value = value[key]
|
|
16
|
+
else:
|
|
17
|
+
raise KeyError(f"{key!r} not in {value.keys()!r}")
|
|
18
|
+
elif isinstance(value, list):
|
|
19
|
+
if isinstance(key, int):
|
|
20
|
+
if 0 <= key < len(value):
|
|
21
|
+
value = value[key]
|
|
22
|
+
else:
|
|
23
|
+
raise IndexError(f"{key} out of range [0,{len(value)})")
|
|
24
|
+
else:
|
|
25
|
+
raise TypeError(f"list can't be indexed with {key!r}")
|
|
26
|
+
else:
|
|
27
|
+
raise TypeError(f"{type(value)} can't be traversed")
|
|
28
|
+
|
|
29
|
+
if isinstance(value, value_type):
|
|
30
|
+
return value
|
|
31
|
+
else:
|
|
32
|
+
raise TypeError(f"value `{value!r}` doesn't have type {value_type}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def has(value: object, value_type: "type[Value]", *keys: "str | int") -> bool:
|
|
36
|
+
"""
|
|
37
|
+
Check if `value` can be traversed using `keys` to a value of type `value_type`.
|
|
38
|
+
"""
|
|
39
|
+
for key in keys:
|
|
40
|
+
if isinstance(value, dict) and key in value:
|
|
41
|
+
value = value[key]
|
|
42
|
+
elif isinstance(value, list) and isinstance(key, int) and 0 <= key < len(value): # fmt: skip # noqa: E501
|
|
43
|
+
value = value[key]
|
|
44
|
+
else:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
return isinstance(value, value_type)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def json_loaded(value: "Any") -> "Any":
|
|
51
|
+
"""
|
|
52
|
+
Ensure `value` has been loaded as JSON.
|
|
53
|
+
"""
|
|
54
|
+
value = str_decoded(value)
|
|
55
|
+
|
|
56
|
+
if isinstance(value, str):
|
|
57
|
+
value = json.loads(value)
|
|
58
|
+
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def str_decoded(value: str | bytes) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Ensure `value` has been decoded to a string.
|
|
65
|
+
"""
|
|
66
|
+
if isinstance(value, bytes):
|
|
67
|
+
value = value.decode()
|
|
68
|
+
|
|
69
|
+
return value
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING, TypeAlias, TypeVar, overload
|
|
2
2
|
|
|
3
|
+
from ..etloutput import NULL_BOX, NULL_SPAN, Box, Span
|
|
3
4
|
from .document import Document
|
|
4
|
-
from .errors import ResultError
|
|
5
5
|
from .predictionlist import PredictionList
|
|
6
6
|
from .predictions import (
|
|
7
|
-
NULL_BOX,
|
|
8
7
|
NULL_CITATION,
|
|
9
|
-
NULL_SPAN,
|
|
10
|
-
Box,
|
|
11
8
|
Classification,
|
|
12
9
|
DocumentExtraction,
|
|
13
10
|
Extraction,
|
|
@@ -15,7 +12,6 @@ from .predictions import (
|
|
|
15
12
|
FormExtractionType,
|
|
16
13
|
Group,
|
|
17
14
|
Prediction,
|
|
18
|
-
Span,
|
|
19
15
|
Summarization,
|
|
20
16
|
Unbundling,
|
|
21
17
|
)
|
|
@@ -45,7 +41,6 @@ __all__ = (
|
|
|
45
41
|
"Prediction",
|
|
46
42
|
"PredictionList",
|
|
47
43
|
"Result",
|
|
48
|
-
"ResultError",
|
|
49
44
|
"Review",
|
|
50
45
|
"ReviewType",
|
|
51
46
|
"Span",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
|
+
from itertools import chain
|
|
2
3
|
from operator import attrgetter
|
|
3
|
-
from typing import TYPE_CHECKING, List, TypeVar, overload
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Final, List, SupportsIndex, TypeVar, overload
|
|
4
5
|
|
|
5
6
|
from .predictions import (
|
|
6
7
|
Classification,
|
|
@@ -13,25 +14,24 @@ from .predictions import (
|
|
|
13
14
|
Unbundling,
|
|
14
15
|
)
|
|
15
16
|
from .review import Review, ReviewType
|
|
16
|
-
from .task import TaskType
|
|
17
17
|
from .utils import nfilter
|
|
18
18
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
20
|
-
from collections.abc import Callable, Collection, Container, Iterable
|
|
21
|
-
from typing import Any, Final, SupportsIndex
|
|
20
|
+
from collections.abc import Callable, Collection, Container, Iterable, Mapping
|
|
22
21
|
|
|
23
22
|
from typing_extensions import Self
|
|
24
23
|
|
|
24
|
+
from ..etloutput import EtlOutput
|
|
25
25
|
from .document import Document
|
|
26
26
|
from .result import Result
|
|
27
|
-
from .task import Task
|
|
27
|
+
from .task import Task, TaskType
|
|
28
28
|
|
|
29
29
|
PredictionType = TypeVar("PredictionType", bound=Prediction)
|
|
30
30
|
OfType = TypeVar("OfType", bound=Prediction)
|
|
31
31
|
KeyType = TypeVar("KeyType")
|
|
32
32
|
|
|
33
33
|
# Non-None sentinel value to support `PredictionList.where(review=None)`.
|
|
34
|
-
REVIEW_UNSPECIFIED:
|
|
34
|
+
REVIEW_UNSPECIFIED: Final = Review(
|
|
35
35
|
id=None, reviewer_id=None, notes=None, rejected=None, type=None # type: ignore[arg-type]
|
|
36
36
|
)
|
|
37
37
|
|
|
@@ -84,6 +84,43 @@ class PredictionList(List[PredictionType]):
|
|
|
84
84
|
|
|
85
85
|
return self
|
|
86
86
|
|
|
87
|
+
def assign_ocr(
|
|
88
|
+
self,
|
|
89
|
+
etl_outputs: "Mapping[Document, EtlOutput]",
|
|
90
|
+
*,
|
|
91
|
+
tokens: bool = True,
|
|
92
|
+
tables: bool = True,
|
|
93
|
+
) -> "Self":
|
|
94
|
+
"""
|
|
95
|
+
Assign OCR tokens, tables, and/or cells using `etl_outputs`.
|
|
96
|
+
|
|
97
|
+
Use `tokens` or `tables` to skip lookup and assignment of those attributes.
|
|
98
|
+
"""
|
|
99
|
+
extractions_by_document = self.oftype(
|
|
100
|
+
DocumentExtraction,
|
|
101
|
+
).groupby(
|
|
102
|
+
attrgetter("document"),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
for document, extractions in extractions_by_document.items():
|
|
106
|
+
etl_output = etl_outputs[document]
|
|
107
|
+
|
|
108
|
+
for extraction in extractions:
|
|
109
|
+
if tokens:
|
|
110
|
+
extraction.tokens = list(
|
|
111
|
+
filter(
|
|
112
|
+
None,
|
|
113
|
+
map(etl_output.token_for, extraction.spans),
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if tables:
|
|
118
|
+
extraction.table_cells = chain.from_iterable(
|
|
119
|
+
map(etl_output.table_cells_for, extraction.spans)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return self
|
|
123
|
+
|
|
87
124
|
def groupby(
|
|
88
125
|
self, key: "Callable[[PredictionType], KeyType]"
|
|
89
126
|
) -> "dict[KeyType, Self]":
|