indico_toolkit 6.14.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.
- indico_toolkit/__init__.py +24 -0
- indico_toolkit/association/__init__.py +15 -0
- indico_toolkit/association/association.py +65 -0
- indico_toolkit/association/extracted_tokens.py +106 -0
- indico_toolkit/association/line_items.py +189 -0
- indico_toolkit/association/positioning.py +301 -0
- indico_toolkit/association/splitting.py +41 -0
- indico_toolkit/auto_populate/__init__.py +3 -0
- indico_toolkit/auto_populate/populator.py +362 -0
- indico_toolkit/auto_populate/types.py +66 -0
- indico_toolkit/auto_review/__init__.py +6 -0
- indico_toolkit/auto_review/auto_review_functions.py +123 -0
- indico_toolkit/auto_review/auto_reviewer.py +62 -0
- indico_toolkit/client.py +35 -0
- indico_toolkit/errors.py +32 -0
- indico_toolkit/etloutput/__init__.py +210 -0
- indico_toolkit/etloutput/cell.py +47 -0
- indico_toolkit/etloutput/errors.py +16 -0
- indico_toolkit/etloutput/etloutput.py +110 -0
- indico_toolkit/etloutput/range.py +30 -0
- indico_toolkit/etloutput/table.py +44 -0
- indico_toolkit/etloutput/token.py +25 -0
- indico_toolkit/indico_wrapper/__init__.py +15 -0
- indico_toolkit/indico_wrapper/dataset.py +114 -0
- indico_toolkit/indico_wrapper/doc_extraction.py +79 -0
- indico_toolkit/indico_wrapper/download.py +193 -0
- indico_toolkit/indico_wrapper/indico_wrapper.py +119 -0
- indico_toolkit/indico_wrapper/modelop.py +200 -0
- indico_toolkit/indico_wrapper/reviewer.py +111 -0
- indico_toolkit/indico_wrapper/workflow.py +231 -0
- indico_toolkit/metrics/__init__.py +11 -0
- indico_toolkit/metrics/compare_ground_truth.py +123 -0
- indico_toolkit/metrics/compare_models.py +180 -0
- indico_toolkit/metrics/metrics.py +373 -0
- indico_toolkit/metrics/plotting.py +102 -0
- indico_toolkit/ocr/__init__.py +9 -0
- indico_toolkit/ocr/customocr_object.py +42 -0
- indico_toolkit/ocr/ondoc_object.py +93 -0
- indico_toolkit/ocr/standard_object.py +54 -0
- indico_toolkit/pipelines/__init__.py +3 -0
- indico_toolkit/pipelines/file_processing.py +157 -0
- indico_toolkit/polling/__init__.py +8 -0
- indico_toolkit/polling/autoreview.py +204 -0
- indico_toolkit/polling/downstream.py +145 -0
- indico_toolkit/polling/queries.py +68 -0
- indico_toolkit/py.typed +0 -0
- indico_toolkit/results/__init__.py +104 -0
- indico_toolkit/results/document.py +78 -0
- indico_toolkit/results/errors.py +4 -0
- indico_toolkit/results/model.py +60 -0
- indico_toolkit/results/normalization.py +159 -0
- indico_toolkit/results/predictionlist.py +395 -0
- indico_toolkit/results/predictions/__init__.py +88 -0
- indico_toolkit/results/predictions/box.py +58 -0
- indico_toolkit/results/predictions/citation.py +47 -0
- indico_toolkit/results/predictions/classification.py +50 -0
- indico_toolkit/results/predictions/documentextraction.py +160 -0
- indico_toolkit/results/predictions/extraction.py +35 -0
- indico_toolkit/results/predictions/formextraction.py +121 -0
- indico_toolkit/results/predictions/group.py +29 -0
- indico_toolkit/results/predictions/prediction.py +41 -0
- indico_toolkit/results/predictions/span.py +43 -0
- indico_toolkit/results/predictions/summarization.py +103 -0
- indico_toolkit/results/predictions/unbundling.py +51 -0
- indico_toolkit/results/result.py +152 -0
- indico_toolkit/results/review.py +32 -0
- indico_toolkit/results/utilities.py +73 -0
- indico_toolkit/retry.py +94 -0
- indico_toolkit/snapshots/__init__.py +3 -0
- indico_toolkit/snapshots/snapshot.py +340 -0
- indico_toolkit/structure/__init__.py +3 -0
- indico_toolkit/structure/create_structure.py +232 -0
- indico_toolkit/structure/queries.py +119 -0
- indico_toolkit/structure/utils.py +16 -0
- indico_toolkit/types/__init__.py +12 -0
- indico_toolkit/types/classification.py +92 -0
- indico_toolkit/types/extractions.py +227 -0
- indico_toolkit/types/predictions.py +28 -0
- indico_toolkit/types/workflow_object.py +133 -0
- indico_toolkit-6.14.0.dist-info/LICENSE +21 -0
- indico_toolkit-6.14.0.dist-info/METADATA +144 -0
- indico_toolkit-6.14.0.dist-info/RECORD +83 -0
- indico_toolkit-6.14.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from ..results import NULL_BOX, NULL_SPAN, Box, Span
|
|
4
|
+
from ..results.utilities import get, has
|
|
5
|
+
from .cell import Cell, CellType
|
|
6
|
+
from .errors import EtlOutputError, TableCellNotFoundError, TokenNotFoundError
|
|
7
|
+
from .etloutput import EtlOutput
|
|
8
|
+
from .range import Range
|
|
9
|
+
from .table import Table
|
|
10
|
+
from .token import Token
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Awaitable, Callable
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
__all__ = (
|
|
17
|
+
"Box",
|
|
18
|
+
"Cell",
|
|
19
|
+
"CellType",
|
|
20
|
+
"EtlOutput",
|
|
21
|
+
"EtlOutputError",
|
|
22
|
+
"load",
|
|
23
|
+
"load_async",
|
|
24
|
+
"NULL_BOX",
|
|
25
|
+
"NULL_SPAN",
|
|
26
|
+
"Range",
|
|
27
|
+
"Span",
|
|
28
|
+
"Table",
|
|
29
|
+
"TableCellNotFoundError",
|
|
30
|
+
"Token",
|
|
31
|
+
"TokenNotFoundError",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load(
|
|
36
|
+
etl_output_uri: str,
|
|
37
|
+
*,
|
|
38
|
+
reader: "Callable[..., Any]",
|
|
39
|
+
text: bool = True,
|
|
40
|
+
tokens: bool = True,
|
|
41
|
+
tables: bool = False,
|
|
42
|
+
) -> EtlOutput:
|
|
43
|
+
"""
|
|
44
|
+
Load `etl_output_uri` as an ETL Output dataclass. A `reader` function must be
|
|
45
|
+
supplied to read JSON files from disk, storage API, or Indico client.
|
|
46
|
+
|
|
47
|
+
Use `text`, `tokens`, and `tables` to specify what to load.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
result = results.load(submission.result_file, reader=read_uri)
|
|
51
|
+
etl_outputs = {
|
|
52
|
+
document: etloutput.load(document.etl_output_uri, reader=read_uri)
|
|
53
|
+
for document in result.documents
|
|
54
|
+
if not document.failed
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
"""
|
|
58
|
+
etl_output = reader(etl_output_uri)
|
|
59
|
+
tables_uri = etl_output_uri.replace("etl_output.json", "tables.json")
|
|
60
|
+
|
|
61
|
+
if has(etl_output, str, "pages", 0, "page_info"):
|
|
62
|
+
return _load_v1(etl_output, tables_uri, reader, text, tokens, tables)
|
|
63
|
+
else:
|
|
64
|
+
return _load_v3(etl_output, tables_uri, reader, text, tokens, tables)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def load_async(
|
|
68
|
+
etl_output_uri: str,
|
|
69
|
+
*,
|
|
70
|
+
reader: "Callable[..., Awaitable[Any]]",
|
|
71
|
+
text: bool = True,
|
|
72
|
+
tokens: bool = True,
|
|
73
|
+
tables: bool = False,
|
|
74
|
+
) -> EtlOutput:
|
|
75
|
+
"""
|
|
76
|
+
Load `etl_output_uri` as an ETL Output dataclass. A `reader` coroutine must be
|
|
77
|
+
supplied to read JSON files from disk, storage API, or Indico client.
|
|
78
|
+
|
|
79
|
+
Use `text`, `tokens`, and `tables` to specify what to load.
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
result = await results.load_async(submission.result_file, reader=read_uri)
|
|
83
|
+
etl_outputs = {
|
|
84
|
+
document: await etloutput.load_async(document.etl_output_uri, reader=read_uri)
|
|
85
|
+
for document in result.documents
|
|
86
|
+
if not document.failed
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
"""
|
|
90
|
+
etl_output = await reader(etl_output_uri)
|
|
91
|
+
tables_uri = etl_output_uri.replace("etl_output.json", "tables.json")
|
|
92
|
+
|
|
93
|
+
if has(etl_output, str, "pages", 0, "page_info"):
|
|
94
|
+
return await _load_v1_async(
|
|
95
|
+
etl_output, tables_uri, reader, text, tokens, tables
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
return await _load_v3_async(
|
|
99
|
+
etl_output, tables_uri, reader, text, tokens, tables
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _load_v1(
|
|
104
|
+
etl_output: "Any",
|
|
105
|
+
tables_uri: str,
|
|
106
|
+
reader: "Callable[..., Any]",
|
|
107
|
+
text: bool,
|
|
108
|
+
tokens: bool,
|
|
109
|
+
tables: bool,
|
|
110
|
+
) -> EtlOutput:
|
|
111
|
+
if text or tokens:
|
|
112
|
+
pages = tuple(
|
|
113
|
+
reader(get(page, str, "page_info"))
|
|
114
|
+
for page in get(etl_output, list, "pages")
|
|
115
|
+
)
|
|
116
|
+
text_by_page = map(lambda page: get(page, str, "pages", 0, "text"), pages)
|
|
117
|
+
tokens_by_page = map(lambda page: get(page, list, "tokens"), pages)
|
|
118
|
+
else:
|
|
119
|
+
text_by_page = () # type: ignore[assignment]
|
|
120
|
+
tokens_by_page = () # type: ignore[assignment]
|
|
121
|
+
|
|
122
|
+
if tables:
|
|
123
|
+
tables_by_page = reader(tables_uri)
|
|
124
|
+
else:
|
|
125
|
+
tables_by_page = ()
|
|
126
|
+
|
|
127
|
+
return EtlOutput.from_pages(text_by_page, tokens_by_page, tables_by_page)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _load_v3(
|
|
131
|
+
etl_output: "Any",
|
|
132
|
+
tables_uri: str,
|
|
133
|
+
reader: "Callable[..., Any]",
|
|
134
|
+
text: bool,
|
|
135
|
+
tokens: bool,
|
|
136
|
+
tables: bool,
|
|
137
|
+
) -> EtlOutput:
|
|
138
|
+
pages = get(etl_output, list, "pages")
|
|
139
|
+
|
|
140
|
+
if text or tokens:
|
|
141
|
+
text_by_page = map(lambda page: reader(get(page, str, "text")), pages)
|
|
142
|
+
else:
|
|
143
|
+
text_by_page = () # type: ignore[assignment]
|
|
144
|
+
|
|
145
|
+
if tokens:
|
|
146
|
+
tokens_by_page = map(lambda page: reader(get(page, str, "tokens")), pages)
|
|
147
|
+
else:
|
|
148
|
+
tokens_by_page = () # type: ignore[assignment]
|
|
149
|
+
|
|
150
|
+
if tables:
|
|
151
|
+
tables_by_page = reader(tables_uri)
|
|
152
|
+
else:
|
|
153
|
+
tables_by_page = ()
|
|
154
|
+
|
|
155
|
+
return EtlOutput.from_pages(text_by_page, tokens_by_page, tables_by_page)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def _load_v1_async(
|
|
159
|
+
etl_output: "Any",
|
|
160
|
+
tables_uri: str,
|
|
161
|
+
reader: "Callable[..., Awaitable[Any]]",
|
|
162
|
+
text: bool,
|
|
163
|
+
tokens: bool,
|
|
164
|
+
tables: bool,
|
|
165
|
+
) -> EtlOutput:
|
|
166
|
+
if text or tokens:
|
|
167
|
+
pages = [
|
|
168
|
+
await reader(get(page, str, "page_info"))
|
|
169
|
+
for page in get(etl_output, list, "pages")
|
|
170
|
+
]
|
|
171
|
+
text_by_page = map(lambda page: get(page, str, "pages", 0, "text"), pages)
|
|
172
|
+
tokens_by_page = map(lambda page: get(page, list, "tokens"), pages)
|
|
173
|
+
else:
|
|
174
|
+
text_by_page = () # type: ignore[assignment]
|
|
175
|
+
tokens_by_page = () # type: ignore[assignment]
|
|
176
|
+
|
|
177
|
+
if tables:
|
|
178
|
+
tables_by_page = await reader(tables_uri)
|
|
179
|
+
else:
|
|
180
|
+
tables_by_page = ()
|
|
181
|
+
|
|
182
|
+
return EtlOutput.from_pages(text_by_page, tokens_by_page, tables_by_page)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def _load_v3_async(
|
|
186
|
+
etl_output: "Any",
|
|
187
|
+
tables_uri: str,
|
|
188
|
+
reader: "Callable[..., Awaitable[Any]]",
|
|
189
|
+
text: bool,
|
|
190
|
+
tokens: bool,
|
|
191
|
+
tables: bool,
|
|
192
|
+
) -> EtlOutput:
|
|
193
|
+
pages = get(etl_output, list, "pages")
|
|
194
|
+
|
|
195
|
+
if text or tokens:
|
|
196
|
+
text_by_page = [await reader(get(page, str, "text")) for page in pages]
|
|
197
|
+
else:
|
|
198
|
+
text_by_page = () # type: ignore[assignment]
|
|
199
|
+
|
|
200
|
+
if tokens:
|
|
201
|
+
tokens_by_page = [await reader(get(page, str, "tokens")) for page in pages]
|
|
202
|
+
else:
|
|
203
|
+
tokens_by_page = () # type: ignore[assignment]
|
|
204
|
+
|
|
205
|
+
if tables:
|
|
206
|
+
tables_by_page = await reader(tables_uri)
|
|
207
|
+
else:
|
|
208
|
+
tables_by_page = ()
|
|
209
|
+
|
|
210
|
+
return EtlOutput.from_pages(text_by_page, tokens_by_page, tables_by_page)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from ..results import NULL_SPAN, Box, Span
|
|
5
|
+
from ..results.utilities import get
|
|
6
|
+
from .range import Range
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CellType(Enum):
|
|
10
|
+
HEADER = "header"
|
|
11
|
+
CONTENT = "content"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Cell:
|
|
16
|
+
type: CellType
|
|
17
|
+
text: str
|
|
18
|
+
box: Box
|
|
19
|
+
range: Range
|
|
20
|
+
spans: "tuple[Span, ...]"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def span(self) -> Span:
|
|
24
|
+
"""
|
|
25
|
+
Return the first `Span` the cell covers or `NULL_SPAN` otherwise.
|
|
26
|
+
|
|
27
|
+
Empty cells have no spans.
|
|
28
|
+
"""
|
|
29
|
+
return self.spans[0] if self.spans else NULL_SPAN
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def from_dict(cell: object, page: int) -> "Cell":
|
|
33
|
+
"""
|
|
34
|
+
Create a `Cell` from a v1 or v3 cell dictionary.
|
|
35
|
+
"""
|
|
36
|
+
get(cell, dict, "position")["page_num"] = page
|
|
37
|
+
|
|
38
|
+
for doc_offset in get(cell, list, "doc_offsets"):
|
|
39
|
+
doc_offset["page_num"] = page
|
|
40
|
+
|
|
41
|
+
return Cell(
|
|
42
|
+
type=CellType(get(cell, str, "cell_type")),
|
|
43
|
+
text=get(cell, str, "text"),
|
|
44
|
+
box=Box.from_dict(get(cell, dict, "position")),
|
|
45
|
+
range=Range.from_dict(cell),
|
|
46
|
+
spans=tuple(map(Span.from_dict, get(cell, list, "doc_offsets"))),
|
|
47
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class EtlOutputError(Exception):
|
|
2
|
+
"""
|
|
3
|
+
Raised when an error occurs while loading an ETL Output file.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TokenNotFoundError(EtlOutputError):
|
|
8
|
+
"""
|
|
9
|
+
Raised when a Token can't be found for a Prediction.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TableCellNotFoundError(EtlOutputError):
|
|
14
|
+
"""
|
|
15
|
+
Raised when a Table Cell can't be found for a Token.
|
|
16
|
+
"""
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
from bisect import bisect_left, bisect_right
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from operator import attrgetter
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from ..results import Box, Span
|
|
8
|
+
from .errors import TableCellNotFoundError, TokenNotFoundError
|
|
9
|
+
from .table import Table
|
|
10
|
+
from .token import Token
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Iterable
|
|
14
|
+
|
|
15
|
+
from .cell import Cell
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class EtlOutput:
|
|
20
|
+
text: str
|
|
21
|
+
text_on_page: "tuple[str, ...]"
|
|
22
|
+
|
|
23
|
+
tokens: "tuple[Token, ...]"
|
|
24
|
+
tokens_on_page: "tuple[tuple[Token, ...], ...]"
|
|
25
|
+
|
|
26
|
+
tables: "tuple[Table, ...]"
|
|
27
|
+
tables_on_page: "tuple[tuple[Table, ...], ...]"
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def from_pages(
|
|
31
|
+
text_by_page: "Iterable[str]",
|
|
32
|
+
token_dicts_by_page: "Iterable[Iterable[object]]",
|
|
33
|
+
table_dicts_by_page: "Iterable[Iterable[object]]",
|
|
34
|
+
) -> "EtlOutput":
|
|
35
|
+
"""
|
|
36
|
+
Create an `EtlOutput` from v1 or v3 page lists.
|
|
37
|
+
"""
|
|
38
|
+
text_by_page = tuple(text_by_page)
|
|
39
|
+
tokens_by_page = tuple(
|
|
40
|
+
tuple(sorted(map(Token.from_dict, token_dict_page), key=attrgetter("span")))
|
|
41
|
+
for token_dict_page in token_dicts_by_page
|
|
42
|
+
)
|
|
43
|
+
tables_by_page = tuple(
|
|
44
|
+
tuple(sorted(map(Table.from_dict, table_dict_page), key=attrgetter("box")))
|
|
45
|
+
for table_dict_page in table_dicts_by_page
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return EtlOutput(
|
|
49
|
+
text="\n".join(text_by_page),
|
|
50
|
+
text_on_page=text_by_page,
|
|
51
|
+
tokens=tuple(itertools.chain.from_iterable(tokens_by_page)),
|
|
52
|
+
tokens_on_page=tokens_by_page,
|
|
53
|
+
tables=tuple(itertools.chain.from_iterable(tables_by_page)),
|
|
54
|
+
tables_on_page=tables_by_page,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def token_for(self, span: Span) -> Token:
|
|
58
|
+
"""
|
|
59
|
+
Return a `Token` that contains every character from `span`.
|
|
60
|
+
Raise `TokenNotFoundError` if one can't be produced.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
tokens = self.tokens_on_page[span.page]
|
|
64
|
+
first = bisect_right(tokens, span.start, key=attrgetter("span.end"))
|
|
65
|
+
last = bisect_left(tokens, span.end, lo=first, key=attrgetter("span.start"))
|
|
66
|
+
tokens = tokens[first:last]
|
|
67
|
+
|
|
68
|
+
return Token(
|
|
69
|
+
text=self.text[span.slice],
|
|
70
|
+
span=span,
|
|
71
|
+
box=Box(
|
|
72
|
+
page=min(token.box.page for token in tokens),
|
|
73
|
+
top=min(token.box.top for token in tokens),
|
|
74
|
+
left=min(token.box.left for token in tokens),
|
|
75
|
+
right=max(token.box.right for token in tokens),
|
|
76
|
+
bottom=max(token.box.bottom for token in tokens),
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
except (IndexError, ValueError) as error:
|
|
80
|
+
raise TokenNotFoundError(f"no token contains {span!r}") from error
|
|
81
|
+
|
|
82
|
+
def table_cell_for(self, token: Token) -> "tuple[Table, Cell]":
|
|
83
|
+
"""
|
|
84
|
+
Return the `Table` and `Cell` that contain the midpoint of `token`.
|
|
85
|
+
Raise `TableCellNotFoundError` if it's not inside a table cell.
|
|
86
|
+
"""
|
|
87
|
+
token_vmid = (token.box.top + token.box.bottom) // 2
|
|
88
|
+
token_hmid = (token.box.left + token.box.right) // 2
|
|
89
|
+
|
|
90
|
+
for table in self.tables_on_page[token.box.page]:
|
|
91
|
+
if (
|
|
92
|
+
(table.box.top <= token_vmid <= table.box.bottom) and
|
|
93
|
+
(table.box.left <= token_hmid <= table.box.right)
|
|
94
|
+
): # fmt: skip
|
|
95
|
+
break
|
|
96
|
+
else:
|
|
97
|
+
raise TableCellNotFoundError(f"no table contains {token!r}")
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
row_index = bisect_left(
|
|
101
|
+
table.rows, token_vmid, key=lambda row: row[0].box.bottom
|
|
102
|
+
)
|
|
103
|
+
row = table.rows[row_index]
|
|
104
|
+
|
|
105
|
+
cell_index = bisect_left(row, token_hmid, key=attrgetter("box.right"))
|
|
106
|
+
cell = row[cell_index]
|
|
107
|
+
except (IndexError, ValueError) as error:
|
|
108
|
+
raise TableCellNotFoundError(f"no cell contains {token!r}") from error
|
|
109
|
+
|
|
110
|
+
return table, cell
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from ..results.utilities import get
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(order=True, frozen=True)
|
|
7
|
+
class Range:
|
|
8
|
+
row: int
|
|
9
|
+
column: int
|
|
10
|
+
rowspan: int
|
|
11
|
+
columnspan: int
|
|
12
|
+
rows: "tuple[int, ...]"
|
|
13
|
+
columns: "tuple[int, ...]"
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def from_dict(cell: object) -> "Range":
|
|
17
|
+
"""
|
|
18
|
+
Create a `Range` from a v1 or v3 cell dictionary.
|
|
19
|
+
"""
|
|
20
|
+
rows = get(cell, list, "rows")
|
|
21
|
+
columns = get(cell, list, "columns")
|
|
22
|
+
|
|
23
|
+
return Range(
|
|
24
|
+
row=rows[0],
|
|
25
|
+
column=columns[0],
|
|
26
|
+
rowspan=len(rows),
|
|
27
|
+
columnspan=len(columns),
|
|
28
|
+
rows=tuple(rows),
|
|
29
|
+
columns=tuple(columns),
|
|
30
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from operator import attrgetter
|
|
3
|
+
|
|
4
|
+
from ..results import Box
|
|
5
|
+
from ..results.utilities import get
|
|
6
|
+
from .cell import Cell
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class Table:
|
|
11
|
+
box: Box
|
|
12
|
+
cells: "tuple[Cell, ...]"
|
|
13
|
+
rows: "tuple[tuple[Cell, ...], ...]"
|
|
14
|
+
columns: "tuple[tuple[Cell, ...], ...]"
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def from_dict(table: object) -> "Table":
|
|
18
|
+
"""
|
|
19
|
+
Create a `Table` from a v1 or v3 table dictionary.
|
|
20
|
+
"""
|
|
21
|
+
page = get(table, int, "page_num")
|
|
22
|
+
get(table, dict, "position")["page_num"] = page
|
|
23
|
+
|
|
24
|
+
cells = tuple(
|
|
25
|
+
sorted(
|
|
26
|
+
(Cell.from_dict(cell, page) for cell in get(table, list, "cells")),
|
|
27
|
+
key=attrgetter("range"),
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
rows = tuple(
|
|
31
|
+
tuple(cell for cell in cells if row in cell.range.rows)
|
|
32
|
+
for row in range(get(table, int, "num_rows"))
|
|
33
|
+
)
|
|
34
|
+
columns = tuple(
|
|
35
|
+
tuple(cell for cell in cells if column in cell.range.columns)
|
|
36
|
+
for column in range(get(table, int, "num_columns"))
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return Table(
|
|
40
|
+
box=Box.from_dict(get(table, dict, "position")),
|
|
41
|
+
cells=cells,
|
|
42
|
+
rows=rows,
|
|
43
|
+
columns=columns,
|
|
44
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from ..results import Box, Span
|
|
4
|
+
from ..results.utilities import get
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Token:
|
|
9
|
+
text: str
|
|
10
|
+
box: Box
|
|
11
|
+
span: Span
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def from_dict(token: object) -> "Token":
|
|
15
|
+
"""
|
|
16
|
+
Create a `Token` from a v1 or v3 token dictionary.
|
|
17
|
+
"""
|
|
18
|
+
get(token, dict, "position")["page_num"] = get(token, int, "page_num")
|
|
19
|
+
get(token, dict, "doc_offset")["page_num"] = get(token, int, "page_num")
|
|
20
|
+
|
|
21
|
+
return Token(
|
|
22
|
+
text=get(token, str, "text"),
|
|
23
|
+
box=Box.from_dict(get(token, dict, "position")),
|
|
24
|
+
span=Span.from_dict(get(token, dict, "doc_offset")),
|
|
25
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .dataset import Datasets
|
|
2
|
+
from .doc_extraction import DocExtraction
|
|
3
|
+
from .download import Download
|
|
4
|
+
from .indico_wrapper import IndicoWrapper
|
|
5
|
+
from .reviewer import Reviewer
|
|
6
|
+
from .workflow import Workflow
|
|
7
|
+
|
|
8
|
+
__all__ = (
|
|
9
|
+
"Datasets",
|
|
10
|
+
"DocExtraction",
|
|
11
|
+
"Download",
|
|
12
|
+
"IndicoWrapper",
|
|
13
|
+
"Reviewer",
|
|
14
|
+
"Workflow",
|
|
15
|
+
)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from indico import IndicoClient
|
|
4
|
+
from indico.queries import (
|
|
5
|
+
AddDataToWorkflow,
|
|
6
|
+
AddFiles,
|
|
7
|
+
CreateDataset,
|
|
8
|
+
CreateEmptyDataset,
|
|
9
|
+
DeleteDataset,
|
|
10
|
+
GetDataset,
|
|
11
|
+
)
|
|
12
|
+
from indico.types import Dataset, OcrEngine, Workflow
|
|
13
|
+
|
|
14
|
+
from .indico_wrapper import IndicoWrapper
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Datasets(IndicoWrapper):
|
|
18
|
+
def __init__(self, client: IndicoClient):
|
|
19
|
+
self.client = client
|
|
20
|
+
|
|
21
|
+
def get_dataset(self, dataset_id: int):
|
|
22
|
+
return self.client.call(GetDataset(dataset_id))
|
|
23
|
+
|
|
24
|
+
def add_files_to_dataset(self, dataset_id: int, filepaths: List[str]) -> Dataset:
|
|
25
|
+
"""
|
|
26
|
+
Upload documents to an existing dataset and wait for them to OCR
|
|
27
|
+
"""
|
|
28
|
+
dataset = self.client.call(
|
|
29
|
+
AddFiles(
|
|
30
|
+
dataset_id=dataset_id, files=filepaths, autoprocess=True, wait=True
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
return dataset
|
|
34
|
+
|
|
35
|
+
def add_new_files_to_task(self, workflow_id: id, wait: bool = True) -> Workflow:
|
|
36
|
+
"""
|
|
37
|
+
Add newly uploaded documents to an existing teach task given the task's
|
|
38
|
+
associated workflow ID
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
workflow_id (id): workflow ID associated with teach task
|
|
42
|
+
wait (bool, optional): wait for data to be added. Defaults to True.
|
|
43
|
+
"""
|
|
44
|
+
workflow = self.client.call(AddDataToWorkflow(workflow_id, wait))
|
|
45
|
+
if wait:
|
|
46
|
+
print(f"Data added to all teach tasks associated with {workflow.id}")
|
|
47
|
+
return workflow
|
|
48
|
+
|
|
49
|
+
def create_empty_dataset(
|
|
50
|
+
self,
|
|
51
|
+
dataset_name: str,
|
|
52
|
+
dataset_type: str = "DOCUMENT",
|
|
53
|
+
ocr_engine: OcrEngine = OcrEngine.READAPI,
|
|
54
|
+
) -> Dataset:
|
|
55
|
+
"""
|
|
56
|
+
Create an empty dataset
|
|
57
|
+
Args:
|
|
58
|
+
name (str): Name of the dataset
|
|
59
|
+
dataset_type (str, optional): TEXT, IMAGE, or DOCUMENT.
|
|
60
|
+
Defaults to "DOCUMENT".
|
|
61
|
+
"""
|
|
62
|
+
return self.client.call(
|
|
63
|
+
CreateEmptyDataset(dataset_name, dataset_type, ocr_engine)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def create_dataset(
|
|
67
|
+
self,
|
|
68
|
+
filepaths: List[str],
|
|
69
|
+
dataset_name: str,
|
|
70
|
+
ocr_engine: OcrEngine = OcrEngine.READAPI,
|
|
71
|
+
) -> Dataset:
|
|
72
|
+
dataset = self.client.call(
|
|
73
|
+
CreateDataset(
|
|
74
|
+
name=dataset_name,
|
|
75
|
+
files=filepaths,
|
|
76
|
+
ocr_engine=ocr_engine,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
self.dataset_id = dataset.id
|
|
80
|
+
return dataset
|
|
81
|
+
|
|
82
|
+
def delete_dataset(self, dataset_id: int) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Returns True if operation is succesful
|
|
85
|
+
"""
|
|
86
|
+
return self.client.call(DeleteDataset(id=dataset_id))
|
|
87
|
+
|
|
88
|
+
def get_dataset_metadata(self, dataset_id: int) -> List[dict]:
|
|
89
|
+
"""
|
|
90
|
+
Get list of dataset files with information like file name, status, and number of
|
|
91
|
+
pages
|
|
92
|
+
"""
|
|
93
|
+
query = """
|
|
94
|
+
query GetDataset($id: Int) {
|
|
95
|
+
dataset(id: $id) {
|
|
96
|
+
id
|
|
97
|
+
name
|
|
98
|
+
files {
|
|
99
|
+
id
|
|
100
|
+
name
|
|
101
|
+
numPages
|
|
102
|
+
status
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
"""
|
|
107
|
+
dataset = self.graphQL_request(
|
|
108
|
+
graphql_query=query, variables={"id": dataset_id}
|
|
109
|
+
)
|
|
110
|
+
return dataset["dataset"]["files"]
|
|
111
|
+
|
|
112
|
+
def get_col_name_by_id(self, dataset_id: int, col_id: int) -> str:
|
|
113
|
+
dataset = self.get_dataset(dataset_id)
|
|
114
|
+
return next(c.name for c in dataset.datacolumns if c.id == col_id)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from typing import List, Union
|
|
2
|
+
|
|
3
|
+
from indico import IndicoClient
|
|
4
|
+
from indico.queries import DocumentExtraction, Job
|
|
5
|
+
|
|
6
|
+
from ..ocr import CustomOcr, OnDoc, StandardOcr
|
|
7
|
+
from .indico_wrapper import IndicoWrapper
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DocExtraction(IndicoWrapper):
|
|
11
|
+
"""
|
|
12
|
+
Class to support DocumentExtraction-related API calls
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
client: IndicoClient,
|
|
18
|
+
preset_config: str = "standard",
|
|
19
|
+
custom_config: dict = None,
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Args:
|
|
23
|
+
preset_config (str): Options are simple, legacy, detailed, ondocument, and
|
|
24
|
+
standard.
|
|
25
|
+
"""
|
|
26
|
+
self._preset_config = preset_config
|
|
27
|
+
self.client = client
|
|
28
|
+
self.json_config = {"preset_config": preset_config}
|
|
29
|
+
if custom_config:
|
|
30
|
+
self.json_config = custom_config
|
|
31
|
+
|
|
32
|
+
def run_ocr(
|
|
33
|
+
self, filepaths: List[str], text_setting: str = None
|
|
34
|
+
) -> List[Union[StandardOcr, OnDoc, CustomOcr, str]]:
|
|
35
|
+
"""
|
|
36
|
+
Args:
|
|
37
|
+
filepaths (List[str]): List of paths to local documents you would like to
|
|
38
|
+
submit for extraction
|
|
39
|
+
text_setting (str): Options are full_text and page_texts.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
extracted_data (List[Union[StandardOcr, OnDoc, CustomOcr, str]]): data from
|
|
43
|
+
DocumentExtraction converted to OCR objects or string text
|
|
44
|
+
"""
|
|
45
|
+
jobs = self._submit_to_ocr(filepaths)
|
|
46
|
+
extracted_data = []
|
|
47
|
+
for ind, job in enumerate(jobs):
|
|
48
|
+
status = self.get_job_status(job.id, True)
|
|
49
|
+
if status.status == "SUCCESS":
|
|
50
|
+
result = self.get_storage_object(status.result)
|
|
51
|
+
if text_setting == "full_text":
|
|
52
|
+
extracted_data.append(self._convert_ocr_objects(result).full_text)
|
|
53
|
+
elif text_setting == "page_texts":
|
|
54
|
+
extracted_data.append(self._convert_ocr_objects(result).page_texts)
|
|
55
|
+
else:
|
|
56
|
+
extracted_data.append(self._convert_ocr_objects(result))
|
|
57
|
+
else:
|
|
58
|
+
raise RuntimeError(
|
|
59
|
+
f"{filepaths[ind]} {status.status}: {status.result}."
|
|
60
|
+
)
|
|
61
|
+
return extracted_data
|
|
62
|
+
|
|
63
|
+
def _submit_to_ocr(self, filepaths: List[str]) -> List[Job]:
|
|
64
|
+
return self.client.call(
|
|
65
|
+
DocumentExtraction(files=filepaths, json_config=self.json_config)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def _convert_ocr_objects(
|
|
69
|
+
self, extracted_data: Union[List[dict], dict]
|
|
70
|
+
) -> Union[StandardOcr, OnDoc, CustomOcr]:
|
|
71
|
+
if self.json_config == {"preset_config": "ondocument"}:
|
|
72
|
+
return OnDoc(extracted_data)
|
|
73
|
+
elif (
|
|
74
|
+
self.json_config == {"preset_config": "standard"}
|
|
75
|
+
or self.json_config is None
|
|
76
|
+
):
|
|
77
|
+
return StandardOcr(extracted_data)
|
|
78
|
+
else:
|
|
79
|
+
return CustomOcr(extracted_data)
|