pydocmaker 2.2.9__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.
pydocmaker/__init__.py ADDED
@@ -0,0 +1,192 @@
1
+ __version__ = '2.2.9'
2
+
3
+ from pydocmaker.core import DocBuilder, construct, constr, buildingblocks, print_to_pdf, get_latex_compiler, set_latex_compiler, make_pdf_from_tex, show_pdf
4
+ from pydocmaker.util import upload_report_to_redmine, bcolors, txtcolor, colors_dc
5
+
6
+
7
+ from pydocmaker.backend.ex_tex import can_run_pandoc
8
+ from pydocmaker.backend.pdf_maker import get_all_installed_latex_compilers, get_latex_compiler
9
+ from pydocmaker.backend.pandoc_api import pandoc_convert_file, pandoc_set_allowed
10
+
11
+ from pydocmaker.core import DocBuilder as Doc
12
+ from pydocmaker.templating import DocTemplate, TemplateDirSource, register_new_template_dir, get_registered_template_dirs, get_available_template_ids, test_template_exists, remove_from_template_dir
13
+
14
+ from latex import escape as tex_escape
15
+
16
+ try:
17
+ # tests and caches already if pandoc is installed when import is used, so its faster later when we want to use it (or not)
18
+ can_run_pandoc()
19
+ except Exception as err:
20
+ pass
21
+
22
+ def pandoc_set_enabled():
23
+ """short for pandoc_set_allowed(True), which will allow pandoc to be used as a valid conversion option"""
24
+ return pandoc_set_allowed(True)
25
+
26
+ def pandoc_set_disabled():
27
+ """short for pandoc_set_allowed(False), which will disallow pandoc to be used as a valid conversion option"""
28
+ return pandoc_set_allowed(False)
29
+
30
+
31
+ def get_schema():
32
+ return {k: getattr(constr, k)() for k in buildingblocks}
33
+
34
+ def get_example():
35
+ return Doc.get_example()
36
+
37
+
38
+ def load(path):
39
+ """Load a JSON file and return a DocBuilder object.
40
+
41
+ Args:
42
+ path (str or file-like object): The path to the JSON file or a file-like object.
43
+
44
+ Returns:
45
+ DocBuilder: A DocBuilder object initialized with the loaded JSON data.
46
+
47
+ Raises:
48
+ json.JSONDecodeError: If the JSON file is not valid.
49
+ TypeError: If the loaded JSON object is not of type list.
50
+ """
51
+ return DocBuilder.load_json(path)
52
+
53
+ def md2tex(children='', **kwargs):
54
+ """convenience function to quickly convert markdown to tex
55
+
56
+ Args:
57
+ children (str, optional): the markdown string to convert. Defaults to ''.
58
+
59
+ Returns:
60
+ str: the corresponding tex string
61
+ """
62
+ return Doc().add_md(children=children, **kwargs).to_tex(text_only=True)
63
+
64
+
65
+ def mk_chapter(title, description, parent=None, order=None):
66
+ """Creates a new chapter.
67
+
68
+ Args:
69
+ title (str): The title of the chapter.
70
+ description (str): A brief description of the chapter.
71
+ parent (Chapter, optional): The parent chapter. Defaults to None.
72
+ order (int, optional): The order of the chapter. Defaults to None.
73
+
74
+ Returns:
75
+ Chapter: The newly created chapter.
76
+ """
77
+ return DocBuilder.add_chapter(title, description, parent, order)[0]
78
+
79
+
80
+ def mk_meta(project_name, version, description, author, author_email, url, license):
81
+ """
82
+ Generate metadata for the documentation.
83
+
84
+ Args:
85
+ project_name (str): The name of the project.
86
+ version (str): The version of the project.
87
+ description (str): A brief description of the project.
88
+ author (str): The author's name.
89
+ author_email (str): The author's email address.
90
+ url (str): The URL of the project.
91
+ license (str): The license of the project.
92
+
93
+ Returns:
94
+ dict: A dictionary containing the metadata.
95
+ """
96
+ return DocBuilder.add_meta(project_name, version, description, author, author_email, url, license)[0]
97
+
98
+
99
+ def mk_tex(children=None, index=None, chapter=None, color='', end=None, **kwargs):
100
+ """
101
+ Creates a new LaTeX document part.
102
+
103
+ Args:
104
+ children (str or list, optional): The "children" for this element. Either text directly (as string) or a list of other parts.
105
+ index (int, optional): The index where to insert the part. If None, appends to the end.
106
+ chapter (str | int, optional): The chapter name or index where to insert the part. If None, appends to the end.
107
+ color (str, optional): Any color which can be rendered by HTML or LaTeX. Empty string for default.
108
+ end (str, optional): If you want to insert a different line ending (than the default) for this element set this argument to any string. None for default.
109
+ **kwargs: Additional keyword arguments for the document part.
110
+
111
+ Returns:
112
+ dict: The newly created LaTeX document part.
113
+ """
114
+ return DocBuilder().add_tex(children=children, index=index, chapter=chapter, color=color, end=end, **kwargs)[0]
115
+
116
+ def mk_md(children=None, index=None, chapter=None, color='', end=None, **kwargs):
117
+ """
118
+ Creates a new markdown document part.
119
+
120
+ Args:
121
+ children (str or list, optional): The "children" for this element. Either text directly (as string) or a list of other parts.
122
+ index (int, optional): The index where to insert the part. If None, appends to the end.
123
+ chapter (str | int, optional): The chapter name or index where to insert the part. If None, appends to the end.
124
+ color (str, optional): Any color which can be rendered by html or latex. Empty string for default.
125
+ end (str, optional): If you want to insert a different line ending (than the default) for this element set this argument to any string. None for default.
126
+ **kwargs: Additional keyword arguments for the document part.
127
+
128
+ Returns:
129
+ DocBuilder: A new DocBuilder object with the added markdown document part.
130
+ """
131
+ return DocBuilder().add_md(children=children, index=index, chapter=chapter, color=color, end=end, **kwargs)
132
+
133
+
134
+ def mk_pre(children=None, index=None, chapter=None, color='', end=None, **kwargs):
135
+ """
136
+ Creates a preformatted document part.
137
+
138
+ Args:
139
+ children (str or list, optional): The "children" for this element. Either text directly (as string) or a list of other parts.
140
+ index (int, optional): The index where to insert the part. If None, appends to the end.
141
+ chapter (str | int, optional): The chapter name or index where to insert the part. If None, appends to the end.
142
+ color (str, optional): Any color which can be rendered by HTML or LaTeX. Empty string for default.
143
+ end (str, optional): If you want to insert a different line ending (than the default) for this element set this argument to any string. None for default.
144
+ **kwargs: Additional keyword arguments for the document part.
145
+
146
+ Returns:
147
+ dict: The created document part.
148
+ """
149
+ return DocBuilder().add_pre(children=children, index=index, chapter=chapter, color=color, end=end, **kwargs)[0]
150
+
151
+
152
+ def mk_fig(fig=None, caption='', width=0.8, children=None, color='', end=None, **kwargs):
153
+ """make an image document part from a pyplot figure type dict from given image input.
154
+
155
+ Args:
156
+ fig (matplotlib figure, optional): the figure which to upload (or the current figure if None). Defaults to None.
157
+ caption (str, optional): the caption to give to the image. Defaults to ''.
158
+ width (float, optional): The width for the image to have in the document. Defaults to 0.8.
159
+ children (str, optional): A specific name/id to give to the image (will be auto generated if None). Defaults to None.
160
+ index (int, optional): The index where to insert the part. If None, appends to the end.
161
+ chapter (str | int, optional): The chapter name or index where to insert the part. If None, appends to the end.
162
+ color (str, optional): any color which can be rendered by html or latex. Empty string for default.
163
+ end (str, optional): If you want to insert a different line ending (than the default) for this element set this argument to any string. None for default.
164
+
165
+ Returns:
166
+ dict: The created document part.
167
+ """
168
+ return DocBuilder().add_fig(fig=fig, caption=caption, width=width, children=children, color=color, end=end, **kwargs)[0]
169
+
170
+ def mk_image(image, caption='', width=0.8, children=None, color='', end=None, **kwargs):
171
+ """make an image type dict from given image input.
172
+ image can be of type:
173
+ - pyplot figure
174
+ - link to download an image from
175
+ - filelike
176
+ - numpy NxMx1 or NxMx3 matrix
177
+ - PIL image
178
+
179
+ Args:
180
+ im (np.array): the image as NxMx
181
+ caption (str, optional): the caption to give to the image. Defaults to ''.
182
+ width (float, optional): The width for the image to have in the document. Defaults to 0.8.
183
+ children (str, optional): A specific name/id to give to the image (will be auto generated if None). Defaults to None.
184
+ index (int, optional): The index where to insert the part. If None, appends to the end.
185
+ chapter (str | int, optional): The chapter name or index where to insert the part. If None, appends to the end.
186
+ color (str, optional): any color which can be rendered by html or latex. Empty string for default.
187
+ end (str, optional): If you want to insert a different line ending (than the default) for this element set this argument to any string. None for default.
188
+
189
+ """
190
+ return DocBuilder().add_image(image, caption, width, children, color, end, **kwargs)[0]
191
+
192
+
File without changes
@@ -0,0 +1,173 @@
1
+ import abc
2
+ import traceback
3
+ import os
4
+
5
+
6
+
7
+ from jinja2 import Template
8
+
9
+ def _handle_template(template, default_template):
10
+ if template is None:
11
+ template = default_template
12
+
13
+ attachments = {}
14
+ if hasattr(template, 'render'):
15
+ template_obj = template
16
+ elif isinstance(template, str) and os.path.exists(template):
17
+ with open(template, 'r') as fp:
18
+ template_obj = Template(fp.read())
19
+ elif isinstance(template, str) and not template:
20
+ template_obj = Template('{{ body }}')
21
+ elif isinstance(template, str):
22
+ template_obj = Template(template)
23
+ else:
24
+ raise KeyError(f'Unknown template type! {type(template)=}')
25
+ return template_obj, attachments
26
+
27
+
28
+
29
+ class BaseFormatter(abc.ABC):
30
+
31
+ default_linebreak = '\n\n'
32
+
33
+ def digest_iterator(self, **kwargs):
34
+ content = kwargs.get('children', kwargs.get('content'))
35
+ return f''.join([self.digest(c) for c in content])
36
+
37
+ def digest_str(self, el):
38
+ return str(el)
39
+
40
+ def digest_text(self, children='', **kwargs) -> list:
41
+ return self.digest(children) # will result in digest_str being called
42
+
43
+ def digest_line(self, **kwargs):
44
+ return self.digest_text(**kwargs)
45
+
46
+ def digest_meta(self, **kwargs):
47
+ return '' # meta element will not influence the rendering and is just ignored
48
+
49
+ @abc.abstractmethod
50
+ def digest_table(self, children=None, **kwargs) -> str:
51
+ pass
52
+
53
+ @abc.abstractmethod
54
+ def digest_markdown(self, children='', **kwargs) -> str:
55
+ pass
56
+
57
+ @abc.abstractmethod
58
+ def digest_image(self, children='', width=0.8, caption='', imageblob='', **kwargs) -> str:
59
+ pass
60
+
61
+ @abc.abstractmethod
62
+ def digest_verbatim(self, children='', **kwargs) -> str:
63
+ pass
64
+
65
+ @abc.abstractmethod
66
+ def digest_latex(self, children:str, **kwargs):
67
+ pass
68
+
69
+ def digest(self, children, **kwargs) -> str:
70
+ try:
71
+
72
+ if not children:
73
+ ret = ''
74
+ elif isinstance(children, str):
75
+ ret = self.digest_str(children)
76
+ elif isinstance(children, dict) and children.get('typ', None) == 'meta':
77
+ ret = self.digest_meta(children=children, **kwargs)
78
+ elif isinstance(children, dict) and children.get('typ', None) == 'table':
79
+ ret = self.digest_table(**children, **kwargs)
80
+ elif isinstance(children, dict) and children.get('typ', None) == 'iter':
81
+ ret = self.digest_iterator(children=children, **kwargs)
82
+ elif isinstance(children, list) and children:
83
+ ret = self.digest_iterator(children=children, **kwargs)
84
+ elif isinstance(children, dict) and children.get('typ', None) == 'image':
85
+ ret = self.digest_image(**children)
86
+ elif isinstance(children, dict) and children.get('typ', None) == 'text':
87
+ ret = self.digest_text(**children)
88
+ elif isinstance(children, dict) and children.get('typ', None) == 'latex':
89
+ ret = self.digest_latex(**children)
90
+ elif isinstance(children, dict) and children.get('typ', None) == 'line':
91
+ ret = self.digest_line(**children)
92
+ elif isinstance(children, dict) and 'typ' in children and children['typ'] == 'verbatim':
93
+ ret = self.digest_verbatim(**children)
94
+ elif isinstance(children, dict) and 'typ' in children and children['typ'] == 'markdown':
95
+ ret = self.digest_markdown(**children)
96
+ else:
97
+ ret = self.handle_error(f'the element of type {type(children)} {children=}, could not be parsed.', children)
98
+
99
+ if isinstance(ret, str):
100
+ linebreak = self.default_linebreak
101
+ if isinstance(children, dict):
102
+ tmp = children.get('end', None)
103
+ if not tmp is None:
104
+ linebreak = tmp
105
+
106
+ tmp = kwargs.get('end', None)
107
+ if not tmp is None:
108
+ linebreak = tmp
109
+
110
+ ret += linebreak
111
+
112
+ return ret
113
+
114
+ except Exception as err:
115
+ return self.handle_error(err, children)
116
+
117
+
118
+ def handle_error(self, err, el=None) -> list:
119
+ e = str(el)
120
+ if len(e) > 300:
121
+ e = e[:300] + f'... (n={len(e)-300} more chars hidden)'
122
+
123
+ txt = 'ERROR WHILE HANDLING ELEMENT:\n{}\n\n'.format(e)
124
+ if not isinstance(err, str):
125
+ txt += '\n'.join(traceback.format_exception(type(err), value=err, tb=err.__traceback__, limit=5))
126
+ else:
127
+ txt += err
128
+ return self.digest_verbatim(children=(txt + '\n'), color='red')
129
+
130
+
131
+ def format(self, doc:list) -> str:
132
+ if hasattr(doc, 'dump'):
133
+ doc = doc.dump()
134
+ if not isinstance(doc, list):
135
+ doc = [doc]
136
+ return ''.join([self.digest(p) for p in doc])
137
+
138
+
139
+
140
+ def _map_table2mat(self, children=None, **kwargs) -> str:
141
+ if children is None:
142
+ children = [[]]
143
+
144
+ assert isinstance(children, (list, tuple)), f'children must be of type list! but was {type(children)=} {children=}'
145
+ header = kwargs.get('header', None)
146
+ header = list(header) if header else []
147
+
148
+ assert isinstance(header, (list, tuple)), f'header must be of type list! but was {type(header)=} {header=}'
149
+ data = list(children)
150
+ wrong_rows = [row for row in children if not isinstance(row, (list, tuple))]
151
+ assert not wrong_rows, f'all rows must be of type list! but found {wrong_rows=}'
152
+
153
+ n_rows = kwargs.get('n_rows', None)
154
+ if n_rows is None:
155
+ n_rows = len(data)
156
+ n_cols = kwargs.get('n_cols', None)
157
+ if n_cols is None:
158
+ n_cols = max(len(header), max([len(row) for row in data]))
159
+
160
+ head = [self.digest(el) for el in header]
161
+ if len(head) < n_cols:
162
+ head += ['']*(n_cols-len(head))
163
+
164
+ mat = []
165
+ for i in range(n_rows):
166
+ mat.append(['']*n_cols)
167
+
168
+ for irow, row in enumerate(data):
169
+ for icol, el in enumerate(row):
170
+ mat[irow][icol] = self.digest(el)
171
+
172
+ return head, mat
173
+
@@ -0,0 +1,174 @@
1
+ import traceback
2
+ import io
3
+
4
+ import base64
5
+ from typing import List
6
+
7
+ import docx
8
+ from docx.shared import Inches, Pt
9
+
10
+ import tempfile
11
+ import os
12
+
13
+ import markdown
14
+
15
+ try:
16
+ from pydocmaker.backend.baseformatter import BaseFormatter
17
+ except Exception as err:
18
+ from .baseformatter import BaseFormatter
19
+
20
+ can_run_pandoc = lambda : False
21
+
22
+
23
+ try:
24
+ from pydocmaker.backend.pandoc_api import can_run_pandoc, pandoc_convert, pandoc_convert_file
25
+ except Exception as err:
26
+ from .pandoc_api import can_run_pandoc, pandoc_convert, pandoc_convert_file
27
+
28
+ try:
29
+ from pydocmaker.backend.ex_html import convert as convert_html
30
+ except Exception as err:
31
+ from .ex_html import convert as convert_html
32
+
33
+
34
+
35
+ def blue(run):
36
+ run.font.color.rgb = docx.shared.RGBColor(0, 0, 255)
37
+
38
+ def red(run):
39
+ run.font.color.rgb = docx.shared.RGBColor(255, 0, 0)
40
+
41
+ def convert_pandoc(doc:List[dict]) -> bytes:
42
+
43
+ with tempfile.TemporaryDirectory() as temp_dir:
44
+ html_file_path = os.path.join(temp_dir, 'temp.html')
45
+ docx_file_path = os.path.join(temp_dir, 'temp.docx')
46
+
47
+ with open(html_file_path, 'w', encoding='utf-8') as fp:
48
+ fp.write(convert_html(doc))
49
+
50
+ pandoc_convert_file(html_file_path, docx_file_path)
51
+ with open(docx_file_path, 'rb') as fp:
52
+ return fp.read()
53
+
54
+ def convert(doc:List[dict]) -> bytes:
55
+
56
+ if can_run_pandoc():
57
+ return convert_pandoc(doc)
58
+ else:
59
+ renderer = docx_renderer()
60
+ renderer.digest(doc)
61
+ return renderer.doc_to_bytes()
62
+
63
+ class docx_renderer(BaseFormatter):
64
+ def __init__(self, template_path:str=None, make_blue=False) -> None:
65
+ self.d = docx.Document(template_path)
66
+ self.make_blue = make_blue
67
+
68
+ def add_paragraph(self, newtext, *args, **kwargs):
69
+ new_paragraph = self.d.add_paragraph(newtext, *args, **kwargs)
70
+ if self.make_blue:
71
+ for r in new_paragraph.runs:
72
+ blue(r)
73
+ return new_paragraph
74
+
75
+ def add_run(self, text, *args, **kwargs):
76
+ if not self.d.paragraphs:
77
+ self.add_paragraph('')
78
+
79
+ last_paragraph = self.d.paragraphs[-1]
80
+
81
+ if not last_paragraph.runs:
82
+ last_run = last_paragraph.add_run(text)
83
+ else:
84
+ last_run = last_paragraph.runs[-1]
85
+ last_run.add_text(text)
86
+
87
+ if self.make_blue:
88
+ blue(last_run)
89
+ return last_run
90
+
91
+ def digest_text(self, children, *args, **kwargs):
92
+ return self.add_paragraph(children)
93
+
94
+
95
+ def digest_str(self, children, *args, **kwargs):
96
+ return self.add_run(children)
97
+
98
+ def digest_line(self, children, *args, **kwargs):
99
+ return self.add_run(children + '\n')
100
+
101
+ def digest_markdown(self, children, *args, **kwargs):
102
+ return self.add_paragraph(children, style='Normal')
103
+
104
+ def digest_verbatim(self, children, *args, **kwargs):
105
+ new_run = self.add_run(children)
106
+ new_run.font.name = 'Courier New' # Or any other monospace font
107
+ new_run.font.size = docx.shared.Pt(8) # Adjust font size as needed
108
+ return new_run
109
+
110
+ def digest_latex(self, children, *args, **kwargs):
111
+ new_run = self.add_run(children)
112
+ new_run.font.name = 'Courier New' # Or any other monospace font
113
+ new_run.font.size = docx.shared.Pt(8) # Adjust font size as needed
114
+ return new_run
115
+
116
+
117
+ def handle_error(self, err, el=None) -> list:
118
+ if isinstance(err, BaseException):
119
+ traceback.print_exc(limit=5)
120
+ err = '\n'.join(traceback.format_exception(type(err), value=err, tb=err.__traceback__, limit=5))
121
+
122
+ new_run = self.add_run(err)
123
+ new_run.font.name = 'Courier New' # Or any other monospace font
124
+ new_run.font.size = docx.shared.Pt(8) # Adjust font size as needed
125
+ red(new_run)
126
+ return new_run
127
+
128
+
129
+ def digest_iterator(self, children, *args, **kwargs):
130
+ if children:
131
+ return [self.digest(val, *args, **kwargs) for val in children]
132
+ return []
133
+
134
+ def digest_table(self, children=None, **kwargs) -> str:
135
+ self.handle_error(NotImplementedError(f'exporter of type {type(self)} can not handle tables'))
136
+
137
+ def digest_image(self, children, *args, **kwargs):
138
+
139
+ image_width = Inches(max(1, kwargs.get('width', 0.8)*5))
140
+ image_caption = kwargs.get('caption', '')
141
+ image_blob = kwargs.get('imageblob', '')
142
+
143
+ assert image_blob, 'no image data given!'
144
+
145
+ btsb64 = image_blob.split(',')[-1]
146
+
147
+ # Decode the base64 image
148
+ img_bytes = base64.b64decode(btsb64)
149
+
150
+ # Create an image stream from the bytes
151
+ image_stream = io.BytesIO(img_bytes)
152
+
153
+ picture = self.d.add_picture(image_stream, width=image_width)
154
+ # picture.width = image_width # Ensure fixed width
155
+ # picture.height = None # Adjust height automatically
156
+ picture.alignment = 1
157
+
158
+ run = self.add_paragraph(image_caption)
159
+ # run.style = 'Caption' # Apply the 'Caption' style for formatting
160
+
161
+ return run
162
+
163
+ def format(self, *args, **kwargs):
164
+ raise NotImplementedError('Can not format a docx document directly')
165
+
166
+
167
+ def doc_to_bytes(self):
168
+ with io.BytesIO() as fp:
169
+ self.d.save(fp)
170
+ fp.seek(0)
171
+ return fp.read()
172
+
173
+ def save(self, filepath):
174
+ self.d.save(filepath)