chemdataextractor-lite 1.0.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 (51) hide show
  1. chemdataextractor_lite-1.0.0/LICENSE +50 -0
  2. chemdataextractor_lite-1.0.0/MANIFEST.in +5 -0
  3. chemdataextractor_lite-1.0.0/PKG-INFO +72 -0
  4. chemdataextractor_lite-1.0.0/README.md +56 -0
  5. chemdataextractor_lite-1.0.0/chemdataextractor/__init__.py +13 -0
  6. chemdataextractor_lite-1.0.0/chemdataextractor/data/README.md +3 -0
  7. chemdataextractor_lite-1.0.0/chemdataextractor/data/punkt_chem-1.0.pickle +0 -0
  8. chemdataextractor_lite-1.0.0/chemdataextractor/doc/__init__.py +17 -0
  9. chemdataextractor_lite-1.0.0/chemdataextractor/doc/document.py +168 -0
  10. chemdataextractor_lite-1.0.0/chemdataextractor/doc/element.py +59 -0
  11. chemdataextractor_lite-1.0.0/chemdataextractor/doc/figure.py +14 -0
  12. chemdataextractor_lite-1.0.0/chemdataextractor/doc/meta.py +30 -0
  13. chemdataextractor_lite-1.0.0/chemdataextractor/doc/table.py +36 -0
  14. chemdataextractor_lite-1.0.0/chemdataextractor/doc/text.py +224 -0
  15. chemdataextractor_lite-1.0.0/chemdataextractor/errors.py +9 -0
  16. chemdataextractor_lite-1.0.0/chemdataextractor/nlp/__init__.py +8 -0
  17. chemdataextractor_lite-1.0.0/chemdataextractor/nlp/tokenize.py +1021 -0
  18. chemdataextractor_lite-1.0.0/chemdataextractor/reader/__init__.py +46 -0
  19. chemdataextractor_lite-1.0.0/chemdataextractor/reader/_helpers.py +67 -0
  20. chemdataextractor_lite-1.0.0/chemdataextractor/reader/acs.py +42 -0
  21. chemdataextractor_lite-1.0.0/chemdataextractor/reader/base.py +40 -0
  22. chemdataextractor_lite-1.0.0/chemdataextractor/reader/cssp.py +45 -0
  23. chemdataextractor_lite-1.0.0/chemdataextractor/reader/elsevier.py +247 -0
  24. chemdataextractor_lite-1.0.0/chemdataextractor/reader/markup.py +359 -0
  25. chemdataextractor_lite-1.0.0/chemdataextractor/reader/nlm.py +63 -0
  26. chemdataextractor_lite-1.0.0/chemdataextractor/reader/pdf.py +145 -0
  27. chemdataextractor_lite-1.0.0/chemdataextractor/reader/plaintext.py +35 -0
  28. chemdataextractor_lite-1.0.0/chemdataextractor/reader/rsc.py +93 -0
  29. chemdataextractor_lite-1.0.0/chemdataextractor/reader/springer.py +122 -0
  30. chemdataextractor_lite-1.0.0/chemdataextractor/reader/springer_jats.py +88 -0
  31. chemdataextractor_lite-1.0.0/chemdataextractor/reader/uspto.py +159 -0
  32. chemdataextractor_lite-1.0.0/chemdataextractor/scrape/__init__.py +15 -0
  33. chemdataextractor_lite-1.0.0/chemdataextractor/scrape/clean.py +218 -0
  34. chemdataextractor_lite-1.0.0/chemdataextractor/scrape/csstranslator.py +120 -0
  35. chemdataextractor_lite-1.0.0/chemdataextractor/text/__init__.py +375 -0
  36. chemdataextractor_lite-1.0.0/chemdataextractor_lite.egg-info/PKG-INFO +72 -0
  37. chemdataextractor_lite-1.0.0/chemdataextractor_lite.egg-info/SOURCES.txt +49 -0
  38. chemdataextractor_lite-1.0.0/chemdataextractor_lite.egg-info/dependency_links.txt +1 -0
  39. chemdataextractor_lite-1.0.0/chemdataextractor_lite.egg-info/requires.txt +7 -0
  40. chemdataextractor_lite-1.0.0/chemdataextractor_lite.egg-info/top_level.txt +1 -0
  41. chemdataextractor_lite-1.0.0/pyproject.toml +29 -0
  42. chemdataextractor_lite-1.0.0/setup.cfg +4 -0
  43. chemdataextractor_lite-1.0.0/tests/data/acs/acs.jmedchem.6b00723.html +6179 -0
  44. chemdataextractor_lite-1.0.0/tests/data/elsevier/j.jnoncrysol.2017.07.006.xml +2836 -0
  45. chemdataextractor_lite-1.0.0/tests/data/elsevier/j.jnoncrysol.2018.02.024.xml +6035 -0
  46. chemdataextractor_lite-1.0.0/tests/data/rsc/10.1039_C6OB02074G.html +392 -0
  47. chemdataextractor_lite-1.0.0/tests/data/rsc/searchresults.html +3007 -0
  48. chemdataextractor_lite-1.0.0/tests/data/rsc/test_paper.html +452 -0
  49. chemdataextractor_lite-1.0.0/tests/data/springer/spr_test1.xml +17 -0
  50. chemdataextractor_lite-1.0.0/tests/data/uspto/US06840965B2.xml +2135 -0
  51. chemdataextractor_lite-1.0.0/tests/test_lite.py +159 -0
@@ -0,0 +1,50 @@
1
+ ChemDataExtractor v2 is released under the MIT license.
2
+ ===========================================================
3
+
4
+ The MIT License
5
+
6
+ Copyright 2020 Juraj Mavračić, Callum J. Court, Taketomo Isazawa, Jacqueline M. Cole and contributors
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the “Software”), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in
16
+ all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
21
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
23
+ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
24
+
25
+
26
+
27
+ ChemDataExtractor License
28
+ ===========================================================
29
+
30
+ The MIT License
31
+
32
+ Copyright 2017 Matt Swain and contributors
33
+
34
+ Permission is hereby granted, free of charge, to any person obtaining a copy
35
+ of this software and associated documentation files (the "Software"), to deal
36
+ in the Software without restriction, including without limitation the rights
37
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
38
+ copies of the Software, and to permit persons to whom the Software is
39
+ furnished to do so, subject to the following conditions:
40
+
41
+ The above copyright notice and this permission notice shall be included in
42
+ all copies or substantial portions of the Software.
43
+
44
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
45
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
46
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
47
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
48
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
49
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
50
+ THE SOFTWARE.
@@ -0,0 +1,5 @@
1
+ include LICENSE
2
+ include README.md
3
+ recursive-include chemdataextractor/data *.pickle *.md
4
+ recursive-include tests/data *
5
+ global-exclude *.py[cod] __pycache__ *.so *.dylib
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: chemdataextractor-lite
3
+ Version: 1.0.0
4
+ Summary: Lightweight scientific document loaders for HTML, XML, PDF, and plain text.
5
+ License: MIT
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: cssselect>=1.0.1
10
+ Requires-Dist: lxml>=4.9
11
+ Requires-Dist: nltk>=3.8
12
+ Requires-Dist: pdfminer.six>=20220524
13
+ Provides-Extra: test
14
+ Requires-Dist: pytest>=8; extra == "test"
15
+ Dynamic: license-file
16
+
17
+ # ChemDataExtractor Lite
18
+
19
+ ChemDataExtractor Lite converts scientific HTML, XML, PDF, and plain-text files into a consistent `Document` object. It keeps document structure, metadata, tables, figures, references, sentence segmentation, and chemistry-aware word segmentation without the tagging and extraction stack from ChemDataExtractor 2.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install -e .
25
+ ```
26
+
27
+ The chemistry-trained sentence model is included in the package, so document loading and segmentation work offline after installation.
28
+
29
+ ## Usage
30
+
31
+ ```python
32
+ from chemdataextractor import Document
33
+
34
+ document = Document.from_file("article.xml")
35
+
36
+ for element in document:
37
+ if hasattr(element, "text"):
38
+ print(type(element).__name__, element.text)
39
+
40
+ for sentence in document.sentences:
41
+ print(sentence.text)
42
+ print(sentence.raw_tokens)
43
+ ```
44
+
45
+ `Document.from_string` accepts `bytes` or Unicode text. Both loading methods accept an optional `readers` list when explicit format handling is needed.
46
+
47
+ ## Supported readers
48
+
49
+ - Generic HTML and XML
50
+ - ACS, RSC, CSSP, and Springer HTML
51
+ - Elsevier, NLM/JATS, Springer JATS, and USPTO XML
52
+ - PDF through `pdfminer.six`
53
+ - Plain text
54
+
55
+ Publisher-specific readers run before generic fallbacks. Readers preserve titles, headings, paragraphs, citations, footnotes, metadata, figure captions and links, table captions, and raw table cells when the source exposes them.
56
+
57
+ ## Document model
58
+
59
+ The public import namespace remains `chemdataextractor`. Structural classes are available from `chemdataextractor.doc`, and readers from `chemdataextractor.reader`.
60
+
61
+ Extraction APIs from ChemDataExtractor 2 are intentionally absent: models, parsers, records, POS/NER tagging, chemical mention detection, relation extraction, scraping clients, and model downloads are outside this package.
62
+
63
+ ## Development
64
+
65
+ ```bash
66
+ python -m pip install -e .
67
+ python -m pip install pytest build
68
+ python -m pytest
69
+ python -m build
70
+ ```
71
+
72
+ The project is released under the MIT license. The bundled `punkt_chem-1.0.pickle` sentence model is the chemistry Punkt model distributed by the ChemDataExtractor project.
@@ -0,0 +1,56 @@
1
+ # ChemDataExtractor Lite
2
+
3
+ ChemDataExtractor Lite converts scientific HTML, XML, PDF, and plain-text files into a consistent `Document` object. It keeps document structure, metadata, tables, figures, references, sentence segmentation, and chemistry-aware word segmentation without the tagging and extraction stack from ChemDataExtractor 2.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install -e .
9
+ ```
10
+
11
+ The chemistry-trained sentence model is included in the package, so document loading and segmentation work offline after installation.
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ from chemdataextractor import Document
17
+
18
+ document = Document.from_file("article.xml")
19
+
20
+ for element in document:
21
+ if hasattr(element, "text"):
22
+ print(type(element).__name__, element.text)
23
+
24
+ for sentence in document.sentences:
25
+ print(sentence.text)
26
+ print(sentence.raw_tokens)
27
+ ```
28
+
29
+ `Document.from_string` accepts `bytes` or Unicode text. Both loading methods accept an optional `readers` list when explicit format handling is needed.
30
+
31
+ ## Supported readers
32
+
33
+ - Generic HTML and XML
34
+ - ACS, RSC, CSSP, and Springer HTML
35
+ - Elsevier, NLM/JATS, Springer JATS, and USPTO XML
36
+ - PDF through `pdfminer.six`
37
+ - Plain text
38
+
39
+ Publisher-specific readers run before generic fallbacks. Readers preserve titles, headings, paragraphs, citations, footnotes, metadata, figure captions and links, table captions, and raw table cells when the source exposes them.
40
+
41
+ ## Document model
42
+
43
+ The public import namespace remains `chemdataextractor`. Structural classes are available from `chemdataextractor.doc`, and readers from `chemdataextractor.reader`.
44
+
45
+ Extraction APIs from ChemDataExtractor 2 are intentionally absent: models, parsers, records, POS/NER tagging, chemical mention detection, relation extraction, scraping clients, and model downloads are outside this package.
46
+
47
+ ## Development
48
+
49
+ ```bash
50
+ python -m pip install -e .
51
+ python -m pip install pytest build
52
+ python -m pytest
53
+ python -m build
54
+ ```
55
+
56
+ The project is released under the MIT license. The bundled `punkt_chem-1.0.pickle` sentence model is the chemistry Punkt model distributed by the ChemDataExtractor project.
@@ -0,0 +1,13 @@
1
+ """Lightweight scientific document loading."""
2
+
3
+ import logging
4
+
5
+ from .doc.document import Document
6
+
7
+ __title__ = "ChemDataExtractor Lite"
8
+ __version__ = "1.0.0"
9
+ __license__ = "MIT"
10
+
11
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
12
+
13
+ __all__ = ["Document"]
@@ -0,0 +1,3 @@
1
+ # Bundled sentence model
2
+
3
+ `punkt_chem-1.0.pickle` is the chemistry-trained Punkt sentence tokenizer distributed by the ChemDataExtractor project. It is bundled here so sentence segmentation does not perform network access at runtime.
@@ -0,0 +1,17 @@
1
+ """Structural document model."""
2
+
3
+ from .document import Document
4
+ from .element import BaseElement, CaptionedElement
5
+ from .figure import Figure
6
+ from .meta import MetaData
7
+ from .table import Table
8
+ from .text import (
9
+ Caption, Cell, Citation, Footnote, Heading, Paragraph, RichToken, Sentence,
10
+ Span, Text, Title, Token,
11
+ )
12
+
13
+ __all__ = [
14
+ "BaseElement", "Caption", "CaptionedElement", "Cell", "Citation", "Document",
15
+ "Figure", "Footnote", "Heading", "MetaData", "Paragraph", "RichToken",
16
+ "Sentence", "Span", "Table", "Text", "Title", "Token",
17
+ ]
@@ -0,0 +1,168 @@
1
+ """Document container and reader dispatch."""
2
+
3
+ import collections
4
+ import io
5
+ import json
6
+ import logging
7
+ from pathlib import Path
8
+
9
+ from ..errors import ReaderError
10
+ from ..text import get_encoding
11
+ from .element import CaptionedElement
12
+ from .figure import Figure
13
+ from .meta import MetaData
14
+ from .table import Table
15
+ from .text import Caption, Cell, Citation, Footnote, Heading, Paragraph, Sentence, Title
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+
20
+ class Document(collections.abc.Sequence):
21
+ """A format-neutral sequence of structural document elements."""
22
+
23
+ def __init__(self, *elements):
24
+ self._elements = []
25
+ for element in elements:
26
+ if isinstance(element, str):
27
+ element = Paragraph(element)
28
+ elif isinstance(element, bytes):
29
+ encoding = get_encoding(element)
30
+ log.warning("Guessed bytestring encoding as %s", encoding)
31
+ element = Paragraph(element.decode(encoding))
32
+ element.document = self
33
+ self._elements.append(element)
34
+
35
+ def __repr__(self):
36
+ return f"<Document: {len(self)} elements>"
37
+
38
+ __str__ = __repr__
39
+
40
+ def __getitem__(self, index):
41
+ return self.elements[index]
42
+
43
+ def __len__(self):
44
+ return len(self.elements)
45
+
46
+ @property
47
+ def elements(self):
48
+ return self._elements
49
+
50
+ @classmethod
51
+ def from_file(cls, file, fname=None, readers=None):
52
+ if isinstance(file, (str, bytes, Path)):
53
+ path = Path(file)
54
+ with path.open("rb") as stream:
55
+ return cls.from_string(stream.read(), fname=fname or str(path), readers=readers)
56
+ if fname is None and hasattr(file, "name"):
57
+ fname = file.name
58
+ return cls.from_string(file.read(), fname=fname, readers=readers)
59
+
60
+ @classmethod
61
+ def from_string(cls, content, fname=None, readers=None):
62
+ if isinstance(content, str):
63
+ content = content.encode("utf-8")
64
+ if not isinstance(content, bytes):
65
+ raise TypeError("content must be bytes or str")
66
+ if readers is None:
67
+ from ..reader import DEFAULT_READERS
68
+ readers = DEFAULT_READERS
69
+ errors = []
70
+ for reader in readers:
71
+ try:
72
+ if not reader.detect(content, fname=fname):
73
+ continue
74
+ document = reader.readstring(content)
75
+ log.debug("Parsed document with %s", reader.__class__.__name__)
76
+ return document
77
+ except ReaderError as error:
78
+ errors.append(error)
79
+ message = "Unable to read document"
80
+ if errors:
81
+ message += f": {errors[-1]}"
82
+ raise ReaderError(message)
83
+
84
+ def get_element_with_id(self, id):
85
+ pending = list(self.elements)
86
+ while pending:
87
+ element = pending.pop(0)
88
+ if element.id == id:
89
+ return element
90
+ if element.elements:
91
+ pending.extend(element.elements)
92
+ return None
93
+
94
+ def _elements_of_type(self, element_type):
95
+ return [element for element in self.elements if isinstance(element, element_type)]
96
+
97
+ @property
98
+ def figures(self):
99
+ return self._elements_of_type(Figure)
100
+
101
+ @property
102
+ def tables(self):
103
+ return self._elements_of_type(Table)
104
+
105
+ @property
106
+ def citations(self):
107
+ return self._elements_of_type(Citation)
108
+
109
+ @property
110
+ def footnotes(self):
111
+ return self._elements_of_type(Footnote)
112
+
113
+ @property
114
+ def titles(self):
115
+ return self._elements_of_type(Title)
116
+
117
+ @property
118
+ def headings(self):
119
+ return self._elements_of_type(Heading)
120
+
121
+ @property
122
+ def paragraphs(self):
123
+ return self._elements_of_type(Paragraph)
124
+
125
+ @property
126
+ def captions(self):
127
+ return self._elements_of_type(Caption)
128
+
129
+ @property
130
+ def captioned_elements(self):
131
+ return self._elements_of_type(CaptionedElement)
132
+
133
+ @property
134
+ def metadata(self):
135
+ metadata = self._elements_of_type(MetaData)
136
+ return metadata[0] if metadata else None
137
+
138
+ @property
139
+ def sentences(self):
140
+ pending = list(self.elements)
141
+ sentences = []
142
+ while pending:
143
+ element = pending.pop(0)
144
+ if isinstance(element, Sentence) and not isinstance(element, Cell):
145
+ sentences.append(element)
146
+ elif element.elements:
147
+ pending[0:0] = element.elements
148
+ return sentences
149
+
150
+ def heading_for_sentence(self, sentence):
151
+ current_heading = None
152
+ for element in self.elements:
153
+ if isinstance(element, Heading):
154
+ current_heading = element
155
+ continue
156
+ if sentence in (element.elements or []):
157
+ return current_heading
158
+ return None
159
+
160
+ def serialize(self):
161
+ return {"type": "document", "elements": [element.serialize() for element in self.elements]}
162
+
163
+ def to_json(self, *args, **kwargs):
164
+ return json.dumps(self.serialize(), *args, **kwargs)
165
+
166
+ def _repr_html_(self):
167
+ body = "\n".join(element._repr_html_() for element in self.elements if hasattr(element, "_repr_html_"))
168
+ return f'<div class="cde-document">\n{body}\n</div>'
@@ -0,0 +1,59 @@
1
+ """Base classes for document elements."""
2
+
3
+ import json
4
+ from abc import ABCMeta, abstractmethod
5
+
6
+
7
+ class BaseElement(metaclass=ABCMeta):
8
+ def __init__(self, document=None, references=None, id=None, **kwargs):
9
+ self._document = document
10
+ self.id = id
11
+ self.references = list(references or [])
12
+
13
+ def __repr__(self):
14
+ return f"<{self.__class__.__name__}>"
15
+
16
+ @property
17
+ def document(self):
18
+ return self._document
19
+
20
+ @document.setter
21
+ def document(self, document):
22
+ self._document = document
23
+
24
+ @property
25
+ def elements(self):
26
+ return None
27
+
28
+ @abstractmethod
29
+ def serialize(self):
30
+ raise NotImplementedError
31
+
32
+ def to_json(self, *args, **kwargs):
33
+ return json.dumps(self.serialize(), *args, **kwargs)
34
+
35
+
36
+ class CaptionedElement(BaseElement):
37
+ def __init__(self, caption, label=None, **kwargs):
38
+ self.caption = caption
39
+ self.label = label
40
+ super().__init__(**kwargs)
41
+ self.caption.document = self.document
42
+
43
+ def __str__(self):
44
+ return self.caption.text
45
+
46
+ @BaseElement.document.setter
47
+ def document(self, document):
48
+ self._document = document
49
+ self.caption.document = document
50
+
51
+ @property
52
+ def elements(self):
53
+ return [self.caption]
54
+
55
+ def serialize(self):
56
+ data = {"type": self.__class__.__name__, "caption": self.caption.serialize()}
57
+ if self.label is not None:
58
+ data["label"] = self.label
59
+ return data
@@ -0,0 +1,14 @@
1
+ """Figure document element."""
2
+
3
+ from .element import CaptionedElement
4
+
5
+
6
+ class Figure(CaptionedElement):
7
+ def __init__(self, caption, label=None, links=None, **kwargs):
8
+ super().__init__(caption=caption, label=label, **kwargs)
9
+ self.links = list(links or [])
10
+
11
+ def serialize(self):
12
+ data = super().serialize()
13
+ data["links"] = self.links
14
+ return data
@@ -0,0 +1,30 @@
1
+ """Bibliographic metadata document element."""
2
+
3
+ from .element import BaseElement
4
+
5
+
6
+ class MetaData(BaseElement):
7
+ FIELDS = (
8
+ "title", "authors", "publisher", "journal", "volume", "issue",
9
+ "firstpage", "lastpage", "doi", "date", "language", "pdf_url", "html_url",
10
+ )
11
+
12
+ def __init__(self, data, **kwargs):
13
+ super().__init__(**kwargs)
14
+ normalized = {key.lstrip("_"): value for key, value in data.items()}
15
+ self._data = {key: normalized.get(key) for key in self.FIELDS}
16
+
17
+ def __repr__(self):
18
+ return repr({key: value for key, value in self.data.items() if value})
19
+
20
+ def __getattr__(self, name):
21
+ if name in self.FIELDS:
22
+ return self._data[name]
23
+ raise AttributeError(name)
24
+
25
+ @property
26
+ def data(self):
27
+ return dict(self._data)
28
+
29
+ def serialize(self):
30
+ return {"MetaData": {key: value for key, value in self._data.items() if value}}
@@ -0,0 +1,36 @@
1
+ """Structural table document element."""
2
+
3
+ from .element import CaptionedElement
4
+ from .text import Cell
5
+
6
+
7
+ class Table(CaptionedElement):
8
+ def __init__(self, caption, label=None, table_data=None, footnotes=None, **kwargs):
9
+ super().__init__(caption=caption, label=label, **kwargs)
10
+ self.table_data = [
11
+ [cell if isinstance(cell, Cell) else Cell(str(cell)) for cell in row]
12
+ for row in (table_data or [])
13
+ ]
14
+ self.footnotes = list(footnotes or [])
15
+ self.document = self.document
16
+
17
+ @CaptionedElement.document.setter
18
+ def document(self, document):
19
+ self._document = document
20
+ self.caption.document = document
21
+ for row in getattr(self, "table_data", []):
22
+ for cell in row:
23
+ cell.document = document
24
+ for footnote in getattr(self, "footnotes", []):
25
+ footnote.document = document
26
+
27
+ @property
28
+ def elements(self):
29
+ return [cell for row in self.table_data for cell in row] + [self.caption] + self.footnotes
30
+
31
+ def serialize(self):
32
+ data = super().serialize()
33
+ data["rows"] = [[cell.text for cell in row] for row in self.table_data]
34
+ if self.footnotes:
35
+ data["footnotes"] = [footnote.serialize() for footnote in self.footnotes]
36
+ return data