scriptplan 0.9.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.
- scriptplan/__init__.py +22 -0
- scriptplan/cli/__init__.py +7 -0
- scriptplan/cli/main.py +546 -0
- scriptplan/core/__init__.py +0 -0
- scriptplan/core/account.py +125 -0
- scriptplan/core/allocation.py +69 -0
- scriptplan/core/booking.py +39 -0
- scriptplan/core/journal.py +377 -0
- scriptplan/core/leave.py +14 -0
- scriptplan/core/limits.py +354 -0
- scriptplan/core/project.py +924 -0
- scriptplan/core/property.py +1290 -0
- scriptplan/core/resource.py +198 -0
- scriptplan/core/resource_scenario.py +711 -0
- scriptplan/core/scenario.py +5 -0
- scriptplan/core/scenario_data.py +39 -0
- scriptplan/core/shift.py +71 -0
- scriptplan/core/task.py +77 -0
- scriptplan/core/task_scenario.py +1515 -0
- scriptplan/core/timesheet.py +457 -0
- scriptplan/core/working_hours.py +231 -0
- scriptplan/parser/__init__.py +0 -0
- scriptplan/parser/macro_processor.py +264 -0
- scriptplan/parser/tjp.lark +412 -0
- scriptplan/parser/tjp_parser.py +1904 -0
- scriptplan/py.typed +0 -0
- scriptplan/report/__init__.py +75 -0
- scriptplan/report/html_generator.py +477 -0
- scriptplan/report/report.py +466 -0
- scriptplan/report/report_base.py +397 -0
- scriptplan/report/report_context.py +248 -0
- scriptplan/report/resource_report.py +341 -0
- scriptplan/report/table_report.py +693 -0
- scriptplan/report/task_report.py +362 -0
- scriptplan/report/text_report.py +172 -0
- scriptplan/scheduler/__init__.py +0 -0
- scriptplan/scheduler/batch_processor.py +238 -0
- scriptplan/scheduler/scoreboard.py +120 -0
- scriptplan/utils/__init__.py +0 -0
- scriptplan/utils/data_cache.py +46 -0
- scriptplan/utils/logger.py +243 -0
- scriptplan/utils/message_handler.py +515 -0
- scriptplan/utils/time.py +195 -0
- scriptplan-0.9.0.dist-info/METADATA +161 -0
- scriptplan-0.9.0.dist-info/RECORD +49 -0
- scriptplan-0.9.0.dist-info/WHEEL +5 -0
- scriptplan-0.9.0.dist-info/entry_points.txt +2 -0
- scriptplan-0.9.0.dist-info/licenses/LICENSE +201 -0
- scriptplan-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TaskReport - Task list report content generator.
|
|
3
|
+
|
|
4
|
+
This module provides the TaskReport class (equivalent to TaskListRE in Ruby)
|
|
5
|
+
which generates a list of tasks that can optionally have the allocated
|
|
6
|
+
resources nested underneath each task line.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Optional, List, Any
|
|
10
|
+
|
|
11
|
+
from scriptplan.report.table_report import (
|
|
12
|
+
TableReport, ReportTable, ReportTableLine, ReportTableCell, Alignment
|
|
13
|
+
)
|
|
14
|
+
from scriptplan.core.property import PropertyList
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from scriptplan.report.report import Report
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TaskReport(TableReport):
|
|
21
|
+
"""
|
|
22
|
+
Task list report generator.
|
|
23
|
+
|
|
24
|
+
This specialization of TableReport implements a task listing. It generates
|
|
25
|
+
a list of tasks that can optionally have the allocated resources nested
|
|
26
|
+
underneath each task line.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
table: The intermediate table representation
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, report: 'Report'):
|
|
33
|
+
"""
|
|
34
|
+
Initialize TaskReport.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
report: The parent Report object
|
|
38
|
+
"""
|
|
39
|
+
super().__init__(report)
|
|
40
|
+
self.table = ReportTable()
|
|
41
|
+
self.table.self_contained = report.get('selfContained') if report.get('selfContained') is not None else True
|
|
42
|
+
self.table.aux_dir = report.get('auxDir') or ''
|
|
43
|
+
|
|
44
|
+
def generate_intermediate_format(self) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Generate the table in the intermediate format.
|
|
47
|
+
|
|
48
|
+
This method prepares the task list, optionally filters it, generates
|
|
49
|
+
the header row, and then generates a row for each task (and optionally
|
|
50
|
+
nested resources).
|
|
51
|
+
"""
|
|
52
|
+
super().generate_intermediate_format()
|
|
53
|
+
|
|
54
|
+
# Prepare the task list
|
|
55
|
+
task_list = self._prepare_task_list()
|
|
56
|
+
|
|
57
|
+
# Prepare the resource list (for nested resources under tasks)
|
|
58
|
+
resource_list = self._prepare_resource_list()
|
|
59
|
+
|
|
60
|
+
# Generate table header
|
|
61
|
+
columns = self.a('columns') or []
|
|
62
|
+
self._generate_header(columns)
|
|
63
|
+
|
|
64
|
+
# Generate task list with optional nested resources
|
|
65
|
+
self._generate_task_list(task_list, resource_list, columns)
|
|
66
|
+
|
|
67
|
+
def _prepare_task_list(self) -> PropertyList:
|
|
68
|
+
"""
|
|
69
|
+
Prepare and filter the task list.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Filtered and sorted PropertyList of tasks
|
|
73
|
+
"""
|
|
74
|
+
task_list = PropertyList(self.project.tasks)
|
|
75
|
+
|
|
76
|
+
# Include adopted tasks
|
|
77
|
+
if hasattr(task_list, 'includeAdopted'):
|
|
78
|
+
task_list.includeAdopted()
|
|
79
|
+
|
|
80
|
+
# Apply sorting
|
|
81
|
+
sort_tasks = self.a('sortTasks')
|
|
82
|
+
if sort_tasks:
|
|
83
|
+
task_list.setSorting(sort_tasks)
|
|
84
|
+
|
|
85
|
+
# Set query for sorting
|
|
86
|
+
if self.project.reportContexts:
|
|
87
|
+
task_list.query = self.project.reportContexts[-1].query
|
|
88
|
+
|
|
89
|
+
# Filter the list
|
|
90
|
+
task_list = self.filter_task_list(
|
|
91
|
+
task_list,
|
|
92
|
+
resource=None,
|
|
93
|
+
hide_expr=self.a('hideTask'),
|
|
94
|
+
rollup_expr=self.a('rollupTask'),
|
|
95
|
+
open_nodes=self.a('openNodes')
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Sort after filtering
|
|
99
|
+
self._sort_task_list(task_list)
|
|
100
|
+
|
|
101
|
+
# Filter to only leaf tasks if leafTasksOnly is set
|
|
102
|
+
if self.a('leafTasksOnly'):
|
|
103
|
+
leaf_tasks = PropertyList(task_list, copyItems=False)
|
|
104
|
+
for task in task_list:
|
|
105
|
+
if hasattr(task, 'leaf') and task.leaf():
|
|
106
|
+
leaf_tasks.append(task)
|
|
107
|
+
return leaf_tasks
|
|
108
|
+
|
|
109
|
+
return task_list
|
|
110
|
+
|
|
111
|
+
def _prepare_resource_list(self) -> PropertyList:
|
|
112
|
+
"""
|
|
113
|
+
Prepare the resource list for nested display.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Sorted PropertyList of resources
|
|
117
|
+
"""
|
|
118
|
+
resource_list = PropertyList(self.project.resources)
|
|
119
|
+
|
|
120
|
+
for resource in self.project.resources:
|
|
121
|
+
resource_list.append(resource)
|
|
122
|
+
|
|
123
|
+
sort_resources = self.a('sortResources')
|
|
124
|
+
if sort_resources:
|
|
125
|
+
resource_list.setSorting(sort_resources)
|
|
126
|
+
|
|
127
|
+
if self.project.reportContexts:
|
|
128
|
+
resource_list.query = self.project.reportContexts[-1].query
|
|
129
|
+
|
|
130
|
+
self._sort_resource_list(resource_list)
|
|
131
|
+
|
|
132
|
+
return resource_list
|
|
133
|
+
|
|
134
|
+
def _sort_task_list(self, task_list: PropertyList) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Sort the task list according to report settings.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
task_list: The list to sort
|
|
140
|
+
"""
|
|
141
|
+
# Use PropertyList's built-in sorting which sorts by seqno by default
|
|
142
|
+
task_list.sort()
|
|
143
|
+
|
|
144
|
+
def _sort_resource_list(self, resource_list: PropertyList) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Sort the resource list according to report settings.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
resource_list: The list to sort
|
|
150
|
+
"""
|
|
151
|
+
# Use PropertyList's built-in sorting which sorts by seqno by default
|
|
152
|
+
resource_list.sort()
|
|
153
|
+
|
|
154
|
+
def _generate_header(self, columns: List[Any]) -> None:
|
|
155
|
+
"""
|
|
156
|
+
Generate the table header row.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
columns: List of column definitions
|
|
160
|
+
"""
|
|
161
|
+
header_line = ReportTableLine()
|
|
162
|
+
|
|
163
|
+
for column_def in columns:
|
|
164
|
+
# Adjust column period if needed
|
|
165
|
+
scenarios = self.a('scenarios') or []
|
|
166
|
+
# self.adjust_column_period(column_def, task_list, scenarios)
|
|
167
|
+
|
|
168
|
+
cell = self.generate_header_cell(column_def)
|
|
169
|
+
header_line.add_cell(cell)
|
|
170
|
+
|
|
171
|
+
self.table.add_header_line(header_line)
|
|
172
|
+
|
|
173
|
+
def _generate_task_list(self, task_list: PropertyList,
|
|
174
|
+
resource_list: PropertyList,
|
|
175
|
+
columns: List[Any]) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Generate rows for each task in the list.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
task_list: List of tasks to display
|
|
181
|
+
resource_list: List of resources (for nested display)
|
|
182
|
+
columns: Column definitions
|
|
183
|
+
"""
|
|
184
|
+
scenario_indices = self.get_scenario_indices()
|
|
185
|
+
scenario_idx = scenario_indices[0] if scenario_indices else 0
|
|
186
|
+
|
|
187
|
+
for task in task_list:
|
|
188
|
+
# Generate task row
|
|
189
|
+
task_line = self._generate_task_line(task, columns, scenario_idx)
|
|
190
|
+
self.table.add_body_line(task_line)
|
|
191
|
+
|
|
192
|
+
# Optionally generate nested resource rows
|
|
193
|
+
if self._should_show_resources():
|
|
194
|
+
nested_resources = self._get_resources_for_task(
|
|
195
|
+
task, resource_list, scenario_idx
|
|
196
|
+
)
|
|
197
|
+
for resource in nested_resources:
|
|
198
|
+
resource_line = self._generate_resource_line(
|
|
199
|
+
resource, task, columns, scenario_idx
|
|
200
|
+
)
|
|
201
|
+
resource_line.css_class = 'nested_resource'
|
|
202
|
+
self.table.add_body_line(resource_line)
|
|
203
|
+
|
|
204
|
+
def _generate_task_line(self, task: Any, columns: List[Any],
|
|
205
|
+
scenario_idx: int) -> ReportTableLine:
|
|
206
|
+
"""
|
|
207
|
+
Generate a table row for a task.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
task: The task property
|
|
211
|
+
columns: Column definitions
|
|
212
|
+
scenario_idx: Scenario index
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
ReportTableLine for the task
|
|
216
|
+
"""
|
|
217
|
+
line = ReportTableLine(task, scenario_idx)
|
|
218
|
+
line.css_class = 'task_row'
|
|
219
|
+
|
|
220
|
+
for column_def in columns:
|
|
221
|
+
cell = self._generate_task_cell(task, column_def, scenario_idx)
|
|
222
|
+
line.add_cell(cell)
|
|
223
|
+
|
|
224
|
+
return line
|
|
225
|
+
|
|
226
|
+
def _generate_task_cell(self, task: Any, column_def: Any,
|
|
227
|
+
scenario_idx: int) -> ReportTableCell:
|
|
228
|
+
"""
|
|
229
|
+
Generate a cell for a task column.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
task: The task property
|
|
233
|
+
column_def: Column definition
|
|
234
|
+
scenario_idx: Scenario index
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
ReportTableCell for the task column
|
|
238
|
+
"""
|
|
239
|
+
column_id = column_def.id if hasattr(column_def, 'id') else str(column_def)
|
|
240
|
+
|
|
241
|
+
# Handle special columns
|
|
242
|
+
if column_id == 'chart':
|
|
243
|
+
return self._generate_gantt_cell(task, column_def, scenario_idx)
|
|
244
|
+
elif column_id in ('hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly'):
|
|
245
|
+
return self._generate_calendar_cell(task, column_def, scenario_idx)
|
|
246
|
+
|
|
247
|
+
# Standard cell generation
|
|
248
|
+
return self.generate_cell(task, column_def, scenario_idx)
|
|
249
|
+
|
|
250
|
+
def _generate_gantt_cell(self, task: Any, column_def: Any,
|
|
251
|
+
scenario_idx: int) -> ReportTableCell:
|
|
252
|
+
"""
|
|
253
|
+
Generate a Gantt chart cell for a task.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
task: The task property
|
|
257
|
+
column_def: Column definition
|
|
258
|
+
scenario_idx: Scenario index
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
ReportTableCell with Gantt representation
|
|
262
|
+
"""
|
|
263
|
+
# Simplified Gantt - just show start/end dates for now
|
|
264
|
+
start = task.get('start', scenario_idx) if hasattr(task, 'get') else None
|
|
265
|
+
end = task.get('end', scenario_idx) if hasattr(task, 'get') else None
|
|
266
|
+
|
|
267
|
+
text = ''
|
|
268
|
+
if start and end:
|
|
269
|
+
text = f'{start} - {end}'
|
|
270
|
+
|
|
271
|
+
return ReportTableCell(text=text, alignment=Alignment.LEFT)
|
|
272
|
+
|
|
273
|
+
def _generate_calendar_cell(self, task: Any, column_def: Any,
|
|
274
|
+
scenario_idx: int) -> ReportTableCell:
|
|
275
|
+
"""
|
|
276
|
+
Generate a calendar column cell for a task.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
task: The task property
|
|
280
|
+
column_def: Column definition
|
|
281
|
+
scenario_idx: Scenario index
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
ReportTableCell with calendar data
|
|
285
|
+
"""
|
|
286
|
+
# Placeholder - would show effort/work per time period
|
|
287
|
+
return ReportTableCell(text='', alignment=Alignment.RIGHT)
|
|
288
|
+
|
|
289
|
+
def _should_show_resources(self) -> bool:
|
|
290
|
+
"""
|
|
291
|
+
Check if resources should be shown nested under tasks.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
True if resources should be nested
|
|
295
|
+
"""
|
|
296
|
+
# Check for 'resources' column or specific report setting
|
|
297
|
+
columns = self.a('columns') or []
|
|
298
|
+
for col in columns:
|
|
299
|
+
col_id = col.id if hasattr(col, 'id') else str(col)
|
|
300
|
+
if col_id == 'resources':
|
|
301
|
+
return False # Resources shown in column, not nested
|
|
302
|
+
|
|
303
|
+
return self.a('showResources') or False
|
|
304
|
+
|
|
305
|
+
def _get_resources_for_task(self, task: Any, resource_list: PropertyList,
|
|
306
|
+
scenario_idx: int) -> List[Any]:
|
|
307
|
+
"""
|
|
308
|
+
Get resources allocated to a task.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
task: The task
|
|
312
|
+
resource_list: All resources
|
|
313
|
+
scenario_idx: Scenario index
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
List of resources allocated to the task
|
|
317
|
+
"""
|
|
318
|
+
result = []
|
|
319
|
+
start = self.a('start')
|
|
320
|
+
end = self.a('end')
|
|
321
|
+
|
|
322
|
+
for resource in resource_list:
|
|
323
|
+
if hasattr(task, 'hasResourceAllocated'):
|
|
324
|
+
if task.hasResourceAllocated(scenario_idx, (start, end), resource):
|
|
325
|
+
result.append(resource)
|
|
326
|
+
|
|
327
|
+
return result
|
|
328
|
+
|
|
329
|
+
def _generate_resource_line(self, resource: Any, task: Any,
|
|
330
|
+
columns: List[Any],
|
|
331
|
+
scenario_idx: int) -> ReportTableLine:
|
|
332
|
+
"""
|
|
333
|
+
Generate a nested resource row under a task.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
resource: The resource
|
|
337
|
+
task: The parent task
|
|
338
|
+
columns: Column definitions
|
|
339
|
+
scenario_idx: Scenario index
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
ReportTableLine for the nested resource
|
|
343
|
+
"""
|
|
344
|
+
line = ReportTableLine(resource, scenario_idx)
|
|
345
|
+
|
|
346
|
+
for column_def in columns:
|
|
347
|
+
col_id = column_def.id if hasattr(column_def, 'id') else str(column_def)
|
|
348
|
+
|
|
349
|
+
if col_id == 'name':
|
|
350
|
+
# Indent the resource name
|
|
351
|
+
name = resource.get('name') if hasattr(resource, 'get') else str(resource)
|
|
352
|
+
cell = ReportTableCell(
|
|
353
|
+
text=name,
|
|
354
|
+
alignment=Alignment.LEFT,
|
|
355
|
+
indent=1 # Extra indent for nested
|
|
356
|
+
)
|
|
357
|
+
else:
|
|
358
|
+
cell = self.generate_cell(resource, column_def, scenario_idx)
|
|
359
|
+
|
|
360
|
+
line.add_cell(cell)
|
|
361
|
+
|
|
362
|
+
return line
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TextReport - Simple text-based report content generator.
|
|
3
|
+
|
|
4
|
+
This module provides the TextReport class which generates simple text-based
|
|
5
|
+
reports that can contain RichText blocks for header, body, and footer sections.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, Optional, List, Any
|
|
9
|
+
|
|
10
|
+
from scriptplan.report.report_base import ReportBase
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from scriptplan.report.report import Report
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TextReport(ReportBase):
|
|
17
|
+
"""
|
|
18
|
+
Simple text report generator.
|
|
19
|
+
|
|
20
|
+
This report type generates a simple text-based output that can contain
|
|
21
|
+
RichText blocks for prolog, header, center, epilog, etc.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
html_content: Generated HTML content
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, report: 'Report'):
|
|
28
|
+
"""
|
|
29
|
+
Initialize TextReport.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
report: The parent Report object
|
|
33
|
+
"""
|
|
34
|
+
super().__init__(report)
|
|
35
|
+
self.html_content = ''
|
|
36
|
+
|
|
37
|
+
def generate_intermediate_format(self) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Generate the intermediate format for text report.
|
|
40
|
+
|
|
41
|
+
This method processes all RichText elements and prepares them
|
|
42
|
+
for output.
|
|
43
|
+
"""
|
|
44
|
+
super().generate_intermediate_format()
|
|
45
|
+
|
|
46
|
+
# Build the content from various text blocks
|
|
47
|
+
parts = []
|
|
48
|
+
|
|
49
|
+
# Prolog
|
|
50
|
+
prolog = self.a('prolog')
|
|
51
|
+
if prolog:
|
|
52
|
+
parts.append(self._rich_text_to_html(prolog))
|
|
53
|
+
|
|
54
|
+
# Header
|
|
55
|
+
header = self.a('header')
|
|
56
|
+
if header:
|
|
57
|
+
parts.append(f'<div class="tj_header">{self._rich_text_to_html(header)}</div>')
|
|
58
|
+
|
|
59
|
+
# Headline
|
|
60
|
+
headline = self.a('headline')
|
|
61
|
+
if headline:
|
|
62
|
+
parts.append(f'<div class="tj_headline">{self._rich_text_to_html(headline)}</div>')
|
|
63
|
+
|
|
64
|
+
# Left/Center/Right blocks
|
|
65
|
+
left = self.a('left')
|
|
66
|
+
center = self.a('center')
|
|
67
|
+
right = self.a('right')
|
|
68
|
+
|
|
69
|
+
if left or center or right:
|
|
70
|
+
parts.append('<div class="tj_columns">')
|
|
71
|
+
if left:
|
|
72
|
+
parts.append(f'<div class="tj_left">{self._rich_text_to_html(left)}</div>')
|
|
73
|
+
if center:
|
|
74
|
+
parts.append(f'<div class="tj_center">{self._rich_text_to_html(center)}</div>')
|
|
75
|
+
if right:
|
|
76
|
+
parts.append(f'<div class="tj_right">{self._rich_text_to_html(right)}</div>')
|
|
77
|
+
parts.append('</div>')
|
|
78
|
+
|
|
79
|
+
# Caption
|
|
80
|
+
caption = self.a('caption')
|
|
81
|
+
if caption:
|
|
82
|
+
parts.append(f'<div class="tj_caption">{self._rich_text_to_html(caption)}</div>')
|
|
83
|
+
|
|
84
|
+
# Footer
|
|
85
|
+
footer = self.a('footer')
|
|
86
|
+
if footer:
|
|
87
|
+
parts.append(f'<div class="tj_footer">{self._rich_text_to_html(footer)}</div>')
|
|
88
|
+
|
|
89
|
+
# Epilog
|
|
90
|
+
epilog = self.a('epilog')
|
|
91
|
+
if epilog:
|
|
92
|
+
parts.append(self._rich_text_to_html(epilog))
|
|
93
|
+
|
|
94
|
+
self.html_content = '\n'.join(parts)
|
|
95
|
+
|
|
96
|
+
def to_html(self) -> Optional[str]:
|
|
97
|
+
"""
|
|
98
|
+
Convert the text report to HTML.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
HTML string representation
|
|
102
|
+
"""
|
|
103
|
+
return self.html_content if self.html_content else None
|
|
104
|
+
|
|
105
|
+
def to_csv(self) -> Optional[List[List[str]]]:
|
|
106
|
+
"""
|
|
107
|
+
Convert the text report to CSV.
|
|
108
|
+
|
|
109
|
+
Text reports don't have tabular data, so this returns None.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
None (text reports are not suitable for CSV)
|
|
113
|
+
"""
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def to_text(self) -> str:
|
|
117
|
+
"""
|
|
118
|
+
Convert the report to plain text.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Plain text representation
|
|
122
|
+
"""
|
|
123
|
+
parts = []
|
|
124
|
+
|
|
125
|
+
prolog = self.a('prolog')
|
|
126
|
+
if prolog:
|
|
127
|
+
parts.append(self._to_plain_text(prolog))
|
|
128
|
+
|
|
129
|
+
headline = self.a('headline')
|
|
130
|
+
if headline:
|
|
131
|
+
parts.append(self._to_plain_text(headline))
|
|
132
|
+
parts.append('=' * 60)
|
|
133
|
+
|
|
134
|
+
left = self.a('left')
|
|
135
|
+
center = self.a('center')
|
|
136
|
+
right = self.a('right')
|
|
137
|
+
|
|
138
|
+
if left:
|
|
139
|
+
parts.append(self._to_plain_text(left))
|
|
140
|
+
if center:
|
|
141
|
+
parts.append(self._to_plain_text(center))
|
|
142
|
+
if right:
|
|
143
|
+
parts.append(self._to_plain_text(right))
|
|
144
|
+
|
|
145
|
+
caption = self.a('caption')
|
|
146
|
+
if caption:
|
|
147
|
+
parts.append('-' * 60)
|
|
148
|
+
parts.append(self._to_plain_text(caption))
|
|
149
|
+
|
|
150
|
+
epilog = self.a('epilog')
|
|
151
|
+
if epilog:
|
|
152
|
+
parts.append(self._to_plain_text(epilog))
|
|
153
|
+
|
|
154
|
+
return '\n\n'.join(filter(None, parts))
|
|
155
|
+
|
|
156
|
+
def _to_plain_text(self, text: Any) -> str:
|
|
157
|
+
"""
|
|
158
|
+
Convert RichText or string to plain text.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
text: RichText object or string
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Plain text string
|
|
165
|
+
"""
|
|
166
|
+
if text is None:
|
|
167
|
+
return ''
|
|
168
|
+
if hasattr(text, 'to_text'):
|
|
169
|
+
return text.to_text()
|
|
170
|
+
if hasattr(text, 'to_s'):
|
|
171
|
+
return text.to_s()
|
|
172
|
+
return str(text)
|
|
File without changes
|