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.
Files changed (83) hide show
  1. indico_toolkit/__init__.py +24 -0
  2. indico_toolkit/association/__init__.py +15 -0
  3. indico_toolkit/association/association.py +65 -0
  4. indico_toolkit/association/extracted_tokens.py +106 -0
  5. indico_toolkit/association/line_items.py +189 -0
  6. indico_toolkit/association/positioning.py +301 -0
  7. indico_toolkit/association/splitting.py +41 -0
  8. indico_toolkit/auto_populate/__init__.py +3 -0
  9. indico_toolkit/auto_populate/populator.py +362 -0
  10. indico_toolkit/auto_populate/types.py +66 -0
  11. indico_toolkit/auto_review/__init__.py +6 -0
  12. indico_toolkit/auto_review/auto_review_functions.py +123 -0
  13. indico_toolkit/auto_review/auto_reviewer.py +62 -0
  14. indico_toolkit/client.py +35 -0
  15. indico_toolkit/errors.py +32 -0
  16. indico_toolkit/etloutput/__init__.py +210 -0
  17. indico_toolkit/etloutput/cell.py +47 -0
  18. indico_toolkit/etloutput/errors.py +16 -0
  19. indico_toolkit/etloutput/etloutput.py +110 -0
  20. indico_toolkit/etloutput/range.py +30 -0
  21. indico_toolkit/etloutput/table.py +44 -0
  22. indico_toolkit/etloutput/token.py +25 -0
  23. indico_toolkit/indico_wrapper/__init__.py +15 -0
  24. indico_toolkit/indico_wrapper/dataset.py +114 -0
  25. indico_toolkit/indico_wrapper/doc_extraction.py +79 -0
  26. indico_toolkit/indico_wrapper/download.py +193 -0
  27. indico_toolkit/indico_wrapper/indico_wrapper.py +119 -0
  28. indico_toolkit/indico_wrapper/modelop.py +200 -0
  29. indico_toolkit/indico_wrapper/reviewer.py +111 -0
  30. indico_toolkit/indico_wrapper/workflow.py +231 -0
  31. indico_toolkit/metrics/__init__.py +11 -0
  32. indico_toolkit/metrics/compare_ground_truth.py +123 -0
  33. indico_toolkit/metrics/compare_models.py +180 -0
  34. indico_toolkit/metrics/metrics.py +373 -0
  35. indico_toolkit/metrics/plotting.py +102 -0
  36. indico_toolkit/ocr/__init__.py +9 -0
  37. indico_toolkit/ocr/customocr_object.py +42 -0
  38. indico_toolkit/ocr/ondoc_object.py +93 -0
  39. indico_toolkit/ocr/standard_object.py +54 -0
  40. indico_toolkit/pipelines/__init__.py +3 -0
  41. indico_toolkit/pipelines/file_processing.py +157 -0
  42. indico_toolkit/polling/__init__.py +8 -0
  43. indico_toolkit/polling/autoreview.py +204 -0
  44. indico_toolkit/polling/downstream.py +145 -0
  45. indico_toolkit/polling/queries.py +68 -0
  46. indico_toolkit/py.typed +0 -0
  47. indico_toolkit/results/__init__.py +104 -0
  48. indico_toolkit/results/document.py +78 -0
  49. indico_toolkit/results/errors.py +4 -0
  50. indico_toolkit/results/model.py +60 -0
  51. indico_toolkit/results/normalization.py +159 -0
  52. indico_toolkit/results/predictionlist.py +395 -0
  53. indico_toolkit/results/predictions/__init__.py +88 -0
  54. indico_toolkit/results/predictions/box.py +58 -0
  55. indico_toolkit/results/predictions/citation.py +47 -0
  56. indico_toolkit/results/predictions/classification.py +50 -0
  57. indico_toolkit/results/predictions/documentextraction.py +160 -0
  58. indico_toolkit/results/predictions/extraction.py +35 -0
  59. indico_toolkit/results/predictions/formextraction.py +121 -0
  60. indico_toolkit/results/predictions/group.py +29 -0
  61. indico_toolkit/results/predictions/prediction.py +41 -0
  62. indico_toolkit/results/predictions/span.py +43 -0
  63. indico_toolkit/results/predictions/summarization.py +103 -0
  64. indico_toolkit/results/predictions/unbundling.py +51 -0
  65. indico_toolkit/results/result.py +152 -0
  66. indico_toolkit/results/review.py +32 -0
  67. indico_toolkit/results/utilities.py +73 -0
  68. indico_toolkit/retry.py +94 -0
  69. indico_toolkit/snapshots/__init__.py +3 -0
  70. indico_toolkit/snapshots/snapshot.py +340 -0
  71. indico_toolkit/structure/__init__.py +3 -0
  72. indico_toolkit/structure/create_structure.py +232 -0
  73. indico_toolkit/structure/queries.py +119 -0
  74. indico_toolkit/structure/utils.py +16 -0
  75. indico_toolkit/types/__init__.py +12 -0
  76. indico_toolkit/types/classification.py +92 -0
  77. indico_toolkit/types/extractions.py +227 -0
  78. indico_toolkit/types/predictions.py +28 -0
  79. indico_toolkit/types/workflow_object.py +133 -0
  80. indico_toolkit-6.14.0.dist-info/LICENSE +21 -0
  81. indico_toolkit-6.14.0.dist-info/METADATA +144 -0
  82. indico_toolkit-6.14.0.dist-info/RECORD +83 -0
  83. 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)