indico_toolkit 6.14.2__tar.gz → 7.2.0__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.
Files changed (83) hide show
  1. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/PKG-INFO +1 -1
  2. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/__init__.py +1 -1
  3. indico_toolkit-7.2.0/indico_toolkit/etloutput/__init__.py +157 -0
  4. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/etloutput/cell.py +1 -1
  5. indico_toolkit-7.2.0/indico_toolkit/etloutput/errors.py +16 -0
  6. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/etloutput/etloutput.py +27 -27
  7. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/etloutput/range.py +1 -1
  8. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/etloutput/table.py +1 -1
  9. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/etloutput/token.py +1 -1
  10. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/polling/autoreview.py +3 -3
  11. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/__init__.py +28 -28
  12. indico_toolkit-7.2.0/indico_toolkit/results/document.py +57 -0
  13. indico_toolkit-7.2.0/indico_toolkit/results/normalization.py +82 -0
  14. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/predictionlist.py +68 -94
  15. indico_toolkit-7.2.0/indico_toolkit/results/predictions/__init__.py +64 -0
  16. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/predictions/box.py +4 -1
  17. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/predictions/citation.py +4 -4
  18. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/predictions/classification.py +5 -11
  19. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/predictions/documentextraction.py +10 -74
  20. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/predictions/formextraction.py +15 -20
  21. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/predictions/prediction.py +4 -10
  22. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/predictions/span.py +1 -1
  23. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/predictions/summarization.py +38 -13
  24. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/predictions/unbundling.py +7 -7
  25. indico_toolkit-7.2.0/indico_toolkit/results/result.py +136 -0
  26. indico_toolkit-7.2.0/indico_toolkit/results/task.py +32 -0
  27. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/utils.py +27 -1
  28. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/pyproject.toml +1 -1
  29. indico_toolkit-6.14.2/indico_toolkit/etloutput/__init__.py +0 -210
  30. indico_toolkit-6.14.2/indico_toolkit/etloutput/errors.py +0 -16
  31. indico_toolkit-6.14.2/indico_toolkit/results/document.py +0 -84
  32. indico_toolkit-6.14.2/indico_toolkit/results/model.py +0 -60
  33. indico_toolkit-6.14.2/indico_toolkit/results/normalization.py +0 -170
  34. indico_toolkit-6.14.2/indico_toolkit/results/predictions/__init__.py +0 -88
  35. indico_toolkit-6.14.2/indico_toolkit/results/result.py +0 -203
  36. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/LICENSE +0 -0
  37. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/README.md +0 -0
  38. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/association/__init__.py +0 -0
  39. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/association/association.py +0 -0
  40. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/association/extracted_tokens.py +0 -0
  41. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/association/line_items.py +0 -0
  42. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/association/positioning.py +0 -0
  43. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/association/splitting.py +0 -0
  44. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/auto_review/__init__.py +0 -0
  45. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/auto_review/auto_review_functions.py +0 -0
  46. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/auto_review/auto_reviewer.py +0 -0
  47. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/client.py +0 -0
  48. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/errors.py +0 -0
  49. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/indico_wrapper/__init__.py +0 -0
  50. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/indico_wrapper/download.py +0 -0
  51. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/indico_wrapper/indico_wrapper.py +0 -0
  52. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/indico_wrapper/modelop.py +0 -0
  53. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/indico_wrapper/workflow.py +0 -0
  54. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/metrics/__init__.py +0 -0
  55. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/metrics/compare_ground_truth.py +0 -0
  56. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/metrics/compare_models.py +0 -0
  57. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/metrics/metrics.py +0 -0
  58. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/metrics/plotting.py +0 -0
  59. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/ocr/__init__.py +0 -0
  60. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/ocr/ondoc_object.py +0 -0
  61. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/ocr/standard_object.py +0 -0
  62. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/pipelines/__init__.py +0 -0
  63. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/pipelines/file_processing.py +0 -0
  64. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/polling/__init__.py +0 -0
  65. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/polling/downstream.py +0 -0
  66. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/polling/queries.py +0 -0
  67. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/py.typed +0 -0
  68. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/errors.py +0 -0
  69. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/predictions/extraction.py +0 -0
  70. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/predictions/group.py +0 -0
  71. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/results/review.py +0 -0
  72. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/retry.py +0 -0
  73. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/snapshots/__init__.py +0 -0
  74. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/snapshots/snapshot.py +0 -0
  75. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/structure/__init__.py +0 -0
  76. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/structure/create_structure.py +0 -0
  77. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/structure/queries.py +0 -0
  78. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/structure/utils.py +0 -0
  79. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/types/__init__.py +0 -0
  80. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/types/classification.py +0 -0
  81. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/types/extractions.py +0 -0
  82. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/types/predictions.py +0 -0
  83. {indico_toolkit-6.14.2 → indico_toolkit-7.2.0}/indico_toolkit/types/workflow_object.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: indico-toolkit
3
- Version: 6.14.2
3
+ Version: 7.2.0
4
4
  Summary: Classes, functions, and abstractions for Indico IPA
5
5
  License: MIT
6
6
  Author: Indico Solutions Engineering
@@ -21,4 +21,4 @@ __all__ = (
21
21
  "ToolkitStaggeredLoopError",
22
22
  "ToolkitStatusError",
23
23
  )
24
- __version__ = "6.14.2"
24
+ __version__ = "7.2.0"
@@ -0,0 +1,157 @@
1
+ from typing import TYPE_CHECKING, TypeAlias, TypeVar
2
+
3
+ from ..results import NULL_BOX, NULL_SPAN, Box, Span
4
+ from ..results.utils import get, has, json_loaded, str_decoded
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
+
15
+ __all__ = (
16
+ "Box",
17
+ "Cell",
18
+ "CellType",
19
+ "EtlOutput",
20
+ "EtlOutputError",
21
+ "load",
22
+ "load_async",
23
+ "NULL_BOX",
24
+ "NULL_SPAN",
25
+ "Range",
26
+ "Span",
27
+ "Table",
28
+ "TableCellNotFoundError",
29
+ "Token",
30
+ "TokenNotFoundError",
31
+ )
32
+
33
+ Loadable: TypeAlias = "dict[str, object] | list[object] | str | bytes"
34
+ Readable = TypeVar("Readable")
35
+ URI: TypeAlias = str
36
+
37
+
38
+ def load(
39
+ etl_output: "Loadable | Readable",
40
+ *,
41
+ reader: "Callable[[Readable | URI], Loadable]",
42
+ text: bool = True,
43
+ tokens: bool = True,
44
+ tables: bool = True,
45
+ ) -> EtlOutput:
46
+ """
47
+ Load `etl_output` as an `EtlOutput` dataclass.
48
+
49
+ `etl_output` can be a dict, JSON string/bytes, or something that can be read by
50
+ `reader` to produce a loadable type.
51
+
52
+ `reader` must be able to load Indico storage URIs as lists or JSON strings/bytes.
53
+
54
+ Use `text`, `tokens`, and `tables` to specify what not to load.
55
+
56
+ ```
57
+ result = results.load(submission.result_file, reader=read_uri)
58
+ etl_outputs = {
59
+ document: etloutput.load(document.etl_output_uri, reader=read_uri)
60
+ for document in result.documents
61
+ if not document.failed
62
+ }
63
+ ```
64
+ """
65
+ if not isinstance(etl_output, dict):
66
+ if (isinstance(etl_output, str) and etl_output.startswith("{")) or (
67
+ isinstance(etl_output, bytes) and etl_output.startswith(b"{")
68
+ ):
69
+ etl_output = json_loaded(etl_output)
70
+ else:
71
+ etl_output = json_loaded(reader(etl_output)) # type: ignore[arg-type]
72
+
73
+ pages = get(etl_output, list, "pages")
74
+
75
+ if text and has(pages, str, 0, "text"):
76
+ text_pages = map(
77
+ lambda page: str_decoded(reader(get(page, str, "text"))), pages # type: ignore[arg-type]
78
+ )
79
+ else:
80
+ text_pages = () # type: ignore[assignment]
81
+
82
+ if tokens and has(pages, str, 0, "tokens"):
83
+ token_dict_pages = map(
84
+ lambda page: json_loaded(reader(get(page, str, "tokens"))), pages
85
+ )
86
+ else:
87
+ token_dict_pages = () # type: ignore[assignment]
88
+
89
+ if tables and has(pages, str, 0, "tables"):
90
+ table_dict_pages = map(
91
+ lambda page: json_loaded(reader(get(page, str, "tables"))), pages
92
+ )
93
+ else:
94
+ table_dict_pages = () # type: ignore[assignment]
95
+
96
+ return EtlOutput.from_pages(text_pages, token_dict_pages, table_dict_pages)
97
+
98
+
99
+ async def load_async(
100
+ etl_output: "Loadable | Readable",
101
+ *,
102
+ reader: "Callable[[Readable | URI], Awaitable[Loadable]]",
103
+ text: bool = True,
104
+ tokens: bool = True,
105
+ tables: bool = True,
106
+ ) -> EtlOutput:
107
+ """
108
+ Load `etl_output` as an `EtlOutput` dataclass.
109
+
110
+ `etl_output` can be a dict, JSON string/bytes, or something that can be read by
111
+ `reader` to produce a loadable type.
112
+
113
+ `reader` must be able to load Indico storage URIs as lists or JSON strings/bytes.
114
+
115
+ Use `text`, `tokens`, and `tables` to specify what not to load.
116
+
117
+ ```
118
+ result = await results.load_async(submission.result_file, reader=read_uri)
119
+ etl_outputs = {
120
+ document: await etloutput.load_async(document.etl_output_uri, reader=read_uri)
121
+ for document in result.documents
122
+ if not document.failed
123
+ }
124
+ ```
125
+ """
126
+ if not isinstance(etl_output, dict):
127
+ if (isinstance(etl_output, str) and etl_output.startswith("{")) or (
128
+ isinstance(etl_output, bytes) and etl_output.startswith(b"{")
129
+ ):
130
+ etl_output = json_loaded(etl_output)
131
+ else:
132
+ etl_output = json_loaded(await reader(etl_output)) # type: ignore[arg-type]
133
+
134
+ pages = get(etl_output, list, "pages")
135
+
136
+ if text and has(pages, str, 0, "text"):
137
+ text_pages = [
138
+ str_decoded(await reader(get(page, str, "text"))) for page in pages # type: ignore[arg-type]
139
+ ]
140
+ else:
141
+ text_pages = () # type: ignore[assignment]
142
+
143
+ if tokens and has(pages, str, 0, "tokens"):
144
+ token_dict_pages = [
145
+ json_loaded(await reader(get(page, str, "tokens"))) for page in pages
146
+ ]
147
+ else:
148
+ token_dict_pages = () # type: ignore[assignment]
149
+
150
+ if tables and has(pages, str, 0, "tables"):
151
+ table_dict_pages = [
152
+ json_loaded(await reader(get(page, str, "tables"))) for page in pages
153
+ ]
154
+ else:
155
+ table_dict_pages = () # type: ignore[assignment]
156
+
157
+ return EtlOutput.from_pages(text_pages, token_dict_pages, table_dict_pages)
@@ -31,7 +31,7 @@ class Cell:
31
31
  @staticmethod
32
32
  def from_dict(cell: object, page: int) -> "Cell":
33
33
  """
34
- Create a `Cell` from a v1 or v3 cell dictionary.
34
+ Create a `Cell` from a cell dictionary.
35
35
  """
36
36
  get(cell, dict, "position")["page_num"] = page
37
37
 
@@ -0,0 +1,16 @@
1
+ class EtlOutputError(Exception):
2
+ """
3
+ Raised when an error occurs accessing `EtlOutput` values.
4
+ """
5
+
6
+
7
+ class TokenNotFoundError(EtlOutputError):
8
+ """
9
+ Raised when a `Token` can't be found for a `Span`.
10
+ """
11
+
12
+
13
+ class TableCellNotFoundError(EtlOutputError):
14
+ """
15
+ Raised when a `Table` and `Cell` can't be found for a `Token`.
16
+ """
@@ -28,30 +28,30 @@ class EtlOutput:
28
28
 
29
29
  @staticmethod
30
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]]",
31
+ text_pages: "Iterable[str]",
32
+ token_dict_pages: "Iterable[Iterable[object]]",
33
+ table_dict_pages: "Iterable[Iterable[object]]",
34
34
  ) -> "EtlOutput":
35
35
  """
36
- Create an `EtlOutput` from v1 or v3 page lists.
36
+ Create an `EtlOutput` from pages of text, tokens, and tables.
37
37
  """
38
- text_by_page = tuple(text_by_page)
39
- tokens_by_page = tuple(
38
+ text_pages = tuple(text_pages)
39
+ token_pages = tuple(
40
40
  tuple(sorted(map(Token.from_dict, token_dict_page), key=attrgetter("span")))
41
- for token_dict_page in token_dicts_by_page
41
+ for token_dict_page in token_dict_pages
42
42
  )
43
- tables_by_page = tuple(
43
+ table_pages = tuple(
44
44
  tuple(sorted(map(Table.from_dict, table_dict_page), key=attrgetter("box")))
45
- for table_dict_page in table_dicts_by_page
45
+ for table_dict_page in table_dict_pages
46
46
  )
47
47
 
48
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,
49
+ text="\n".join(text_pages),
50
+ text_on_page=text_pages,
51
+ tokens=tuple(itertools.chain.from_iterable(token_pages)),
52
+ tokens_on_page=token_pages,
53
+ tables=tuple(itertools.chain.from_iterable(table_pages)),
54
+ tables_on_page=table_pages,
55
55
  )
56
56
 
57
57
  def token_for(self, span: Span) -> Token:
@@ -64,21 +64,21 @@ class EtlOutput:
64
64
  first = bisect_right(tokens, span.start, key=attrgetter("span.end"))
65
65
  last = bisect_left(tokens, span.end, lo=first, key=attrgetter("span.start"))
66
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
67
  except (IndexError, ValueError) as error:
80
68
  raise TokenNotFoundError(f"no token contains {span!r}") from error
81
69
 
70
+ return Token(
71
+ text=self.text[span.slice],
72
+ box=Box(
73
+ page=span.page,
74
+ top=min(token.box.top for token in tokens),
75
+ left=min(token.box.left for token in tokens),
76
+ right=max(token.box.right for token in tokens),
77
+ bottom=max(token.box.bottom for token in tokens),
78
+ ),
79
+ span=span,
80
+ )
81
+
82
82
  def table_cell_for(self, token: Token) -> "tuple[Table, Cell]":
83
83
  """
84
84
  Return the `Table` and `Cell` that contain the midpoint of `token`.
@@ -15,7 +15,7 @@ class Range:
15
15
  @staticmethod
16
16
  def from_dict(cell: object) -> "Range":
17
17
  """
18
- Create a `Range` from a v1 or v3 cell dictionary.
18
+ Create a `Range` from a cell dictionary.
19
19
  """
20
20
  rows = get(cell, list, "rows")
21
21
  columns = get(cell, list, "columns")
@@ -16,7 +16,7 @@ class Table:
16
16
  @staticmethod
17
17
  def from_dict(table: object) -> "Table":
18
18
  """
19
- Create a `Table` from a v1 or v3 table dictionary.
19
+ Create a `Table` from a table dictionary.
20
20
  """
21
21
  page = get(table, int, "page_num")
22
22
  get(table, dict, "position")["page_num"] = page
@@ -13,7 +13,7 @@ class Token:
13
13
  @staticmethod
14
14
  def from_dict(token: object) -> "Token":
15
15
  """
16
- Create a `Token` from a v1 or v3 token dictionary.
16
+ Create a `Token` from a token dictionary.
17
17
  """
18
18
  get(token, dict, "position")["page_num"] = get(token, int, "page_num")
19
19
  get(token, dict, "doc_offset")["page_num"] = get(token, int, "page_num")
@@ -53,7 +53,7 @@ class AutoReviewPoller:
53
53
  load_etl_output: bool = True,
54
54
  load_text: bool = True,
55
55
  load_tokens: bool = True,
56
- load_tables: bool = False,
56
+ load_tables: bool = True,
57
57
  retry_count: int = 4,
58
58
  retry_wait: float = 1,
59
59
  retry_backoff: float = 4,
@@ -96,8 +96,8 @@ class AutoReviewPoller:
96
96
  *(self._reap_workers() for _ in range(self._worker_count)),
97
97
  )
98
98
 
99
- async def _retrieve_storage_object(self, url: str) -> object:
100
- return await self._client_call(RetrieveStorageObject(url))
99
+ async def _retrieve_storage_object(self, uri: str) -> "Any":
100
+ return await self._client_call(RetrieveStorageObject(uri))
101
101
 
102
102
  async def _spawn_workers(self) -> None:
103
103
  """
@@ -1,9 +1,7 @@
1
- import json
2
- from typing import TYPE_CHECKING
1
+ from typing import TYPE_CHECKING, TypeAlias, TypeVar, overload
3
2
 
4
3
  from .document import Document
5
4
  from .errors import ResultError
6
- from .model import ModelGroup, ModelGroupType
7
5
  from .predictionlist import PredictionList
8
6
  from .predictions import (
9
7
  NULL_BOX,
@@ -23,7 +21,8 @@ from .predictions import (
23
21
  )
24
22
  from .result import Result
25
23
  from .review import Review, ReviewType
26
- from .utils import get
24
+ from .task import Task, TaskType
25
+ from .utils import json_loaded
27
26
 
28
27
  if TYPE_CHECKING:
29
28
  from collections.abc import Awaitable, Callable
@@ -40,8 +39,6 @@ __all__ = (
40
39
  "Group",
41
40
  "load",
42
41
  "load_async",
43
- "ModelGroup",
44
- "ModelGroupType",
45
42
  "NULL_BOX",
46
43
  "NULL_CITATION",
47
44
  "NULL_SPAN",
@@ -53,16 +50,24 @@ __all__ = (
53
50
  "ReviewType",
54
51
  "Span",
55
52
  "Summarization",
53
+ "Task",
54
+ "TaskType",
56
55
  "Unbundling",
57
56
  )
58
57
 
58
+ Loadable: TypeAlias = "dict[str, object] | str | bytes"
59
+ Readable = TypeVar("Readable")
59
60
 
61
+
62
+ @overload
63
+ def load(result: Loadable) -> Result: ...
64
+ @overload
65
+ def load(result: Readable, *, reader: "Callable[[Readable], Loadable]") -> Result: ...
60
66
  def load(result: object, *, reader: "Callable[..., object] | None" = None) -> Result:
61
67
  """
62
- Load `result` as a Result dataclass.
68
+ Load `result` as a `Result` dataclass. `result` can be a dict or JSON string/bytes.
63
69
 
64
- `result` can be a dict, a JSON string, or something that can be read with `reader`
65
- to produce either.
70
+ If `reader` is provided, it should read `result` to produce a loadable type.
66
71
 
67
72
  ```
68
73
  for result_file in result_folder.glob("*.json"):
@@ -72,33 +77,28 @@ def load(result: object, *, reader: "Callable[..., object] | None" = None) -> Re
72
77
  if reader:
73
78
  result = reader(result)
74
79
 
75
- return _load(result)
80
+ return Result.from_dict(json_loaded(result))
76
81
 
77
82
 
83
+ @overload
84
+ async def load_async(result: Loadable) -> Result: ...
85
+ @overload
86
+ async def load_async(
87
+ result: Readable, *, reader: "Callable[[Readable], Awaitable[Loadable]]"
88
+ ) -> Result: ...
78
89
  async def load_async(
79
90
  result: object, *, reader: "Callable[..., Awaitable[object]] | None" = None
80
91
  ) -> Result:
81
92
  """
82
- Load `result` as a Result dataclass.
93
+ Load `result` as a `Result` dataclass. `result` can be a dict or JSON string/bytes.
94
+
95
+ If `reader` is provided, it should read `result` to produce a loadable type.
83
96
 
84
- `result` can be a dict, a JSON string, or something that can be read with `reader`
85
- to produce either.
97
+ ```
98
+ result = await results.load_async(submission.result_file, reader=read_uri)
99
+ ```
86
100
  """
87
101
  if reader:
88
102
  result = await reader(result)
89
103
 
90
- return _load(result)
91
-
92
-
93
- def _load(result: object) -> Result:
94
- if isinstance(result, str) and result.strip().startswith("{"):
95
- result = json.loads(result)
96
-
97
- file_version = get(result, int, "file_version")
98
-
99
- if file_version == 1:
100
- return Result.from_v1_dict(result)
101
- elif file_version == 3:
102
- return Result.from_v3_dict(result)
103
- else:
104
- raise ResultError(f"unsupported file version `{file_version}`")
104
+ return Result.from_dict(json_loaded(result))
@@ -0,0 +1,57 @@
1
+ from dataclasses import dataclass
2
+
3
+ from .utils import get
4
+
5
+
6
+ @dataclass(frozen=True, order=True)
7
+ class Document:
8
+ id: int
9
+ name: str
10
+ etl_output_uri: str
11
+ failed: bool
12
+ error: str
13
+ traceback: str
14
+
15
+ # Auto review changes must reproduce all model and component sections that were
16
+ # present in the original result file. This may not be possible from the
17
+ # predictions alone--if a model or component had an empty section because it didn't
18
+ # produce predictions or if all of the predictions for that section were dropped.
19
+ # As such, the model and component IDs seen when parsing a result file are tracked
20
+ # per-document so that the empty sections can be reproduced later.
21
+ _model_ids: "frozenset[str]"
22
+ _component_ids: "frozenset[str]"
23
+
24
+ @staticmethod
25
+ def from_dict(document: object) -> "Document":
26
+ """
27
+ Create a `Document` from a document dictionary.
28
+ """
29
+ model_results = get(document, dict, "model_results", "ORIGINAL")
30
+ component_results = get(document, dict, "component_results", "ORIGINAL")
31
+
32
+ return Document(
33
+ id=get(document, int, "submissionfile_id"),
34
+ name=get(document, str, "input_filename"),
35
+ etl_output_uri=get(document, str, "etl_output"),
36
+ failed=False,
37
+ error="",
38
+ traceback="",
39
+ _model_ids=frozenset(model_results.keys()),
40
+ _component_ids=frozenset(component_results.keys()),
41
+ )
42
+
43
+ @staticmethod
44
+ def from_errored_file_dict(errored_file: object) -> "Document":
45
+ """
46
+ Create a `Document` from an errored file dictionary.
47
+ """
48
+ return Document(
49
+ id=get(errored_file, int, "submissionfile_id"),
50
+ name=get(errored_file, str, "input_filename"),
51
+ etl_output_uri="",
52
+ failed=True,
53
+ error=get(errored_file, str, "error"),
54
+ traceback=get(errored_file, str, "traceback"),
55
+ _model_ids=frozenset(),
56
+ _component_ids=frozenset(),
57
+ )
@@ -0,0 +1,82 @@
1
+ import re
2
+ from typing import TYPE_CHECKING
3
+
4
+ from .task import TaskType
5
+ from .utils import get, has
6
+
7
+ if TYPE_CHECKING:
8
+ from typing import Any
9
+
10
+
11
+ def normalize_result_dict(result: "Any") -> None:
12
+ """
13
+ Fix inconsistencies observed in result file structure.
14
+ """
15
+ for errored_file in get(result, dict, "errored_files").values():
16
+ # Parse filenames for errored files.
17
+ if not has(errored_file, str, "input_filename"):
18
+ reason = get(errored_file, str, "reason")
19
+ match = re.search(r"file '([^']*)' with id", reason)
20
+ errored_file["input_filename"] = match.group(1) if match else ""
21
+
22
+ # Parse error from trackback for errored files.
23
+ if not has(errored_file, str, "traceback"):
24
+ traceback = get(errored_file, str, "error")
25
+ error = traceback.split("\n")[-1].strip()
26
+ errored_file["traceback"] = traceback
27
+ errored_file["error"] = error
28
+
29
+ # Convert `None` review notes to "".
30
+ for review in get(result, dict, "reviews").values():
31
+ if not has(review, str, "review_notes"):
32
+ review["review_notes"] = ""
33
+
34
+
35
+ def normalize_prediction_dict(task_type: TaskType, prediction: "Any") -> None:
36
+ """
37
+ Fix inconsistencies observed in prediction structure.
38
+ """
39
+ # Predictions added in review lack a `confidence` section.
40
+ # (And should theoretically have 100% confidence.)
41
+ if not has(prediction, dict, "confidence"):
42
+ prediction["confidence"] = {get(prediction, str, "label"): 1.0}
43
+
44
+ # Extractions added in review may lack a `normalized` section.
45
+ if task_type in (
46
+ TaskType.DOCUMENT_EXTRACTION,
47
+ TaskType.FORM_EXTRACTION,
48
+ TaskType.GENAI_EXTRACTION,
49
+ ) and not has(prediction, dict, "normalized"):
50
+ prediction["normalized"] = {"formatted": get(prediction, str, "text")}
51
+
52
+ # Document Extractions added in review may lack a `spans` section.
53
+ # This value will match `NULL_SPAN`.
54
+ if task_type in (
55
+ TaskType.DOCUMENT_EXTRACTION,
56
+ TaskType.GENAI_EXTRACTION,
57
+ ) and not has(prediction, list, "spans"):
58
+ prediction["spans"] = []
59
+
60
+ # Form Extractions added in review may lack bounding box information.
61
+ # These values will match `NULL_BOX`.
62
+ if task_type == TaskType.FORM_EXTRACTION and not has(prediction, int, "top"):
63
+ prediction["page_num"] = 0
64
+ prediction["top"] = 0
65
+ prediction["left"] = 0
66
+ prediction["right"] = 0
67
+ prediction["bottom"] = 0
68
+
69
+ # Document Extractions that didn't go through a linked labels transformer
70
+ # lack a `groupings` section.
71
+ if task_type in (
72
+ TaskType.DOCUMENT_EXTRACTION,
73
+ TaskType.GENAI_EXTRACTION,
74
+ ) and not has(prediction, list, "groupings"):
75
+ prediction["groupings"] = []
76
+
77
+ # Summarizations added in review may lack a `citations` section.
78
+ # This value will match `NULL_CITATION`.
79
+ if task_type == TaskType.GENAI_SUMMARIZATION and not has(
80
+ prediction, list, "citations"
81
+ ):
82
+ prediction["citations"] = []