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.
Files changed (49) hide show
  1. scriptplan/__init__.py +22 -0
  2. scriptplan/cli/__init__.py +7 -0
  3. scriptplan/cli/main.py +546 -0
  4. scriptplan/core/__init__.py +0 -0
  5. scriptplan/core/account.py +125 -0
  6. scriptplan/core/allocation.py +69 -0
  7. scriptplan/core/booking.py +39 -0
  8. scriptplan/core/journal.py +377 -0
  9. scriptplan/core/leave.py +14 -0
  10. scriptplan/core/limits.py +354 -0
  11. scriptplan/core/project.py +924 -0
  12. scriptplan/core/property.py +1290 -0
  13. scriptplan/core/resource.py +198 -0
  14. scriptplan/core/resource_scenario.py +711 -0
  15. scriptplan/core/scenario.py +5 -0
  16. scriptplan/core/scenario_data.py +39 -0
  17. scriptplan/core/shift.py +71 -0
  18. scriptplan/core/task.py +77 -0
  19. scriptplan/core/task_scenario.py +1515 -0
  20. scriptplan/core/timesheet.py +457 -0
  21. scriptplan/core/working_hours.py +231 -0
  22. scriptplan/parser/__init__.py +0 -0
  23. scriptplan/parser/macro_processor.py +264 -0
  24. scriptplan/parser/tjp.lark +412 -0
  25. scriptplan/parser/tjp_parser.py +1904 -0
  26. scriptplan/py.typed +0 -0
  27. scriptplan/report/__init__.py +75 -0
  28. scriptplan/report/html_generator.py +477 -0
  29. scriptplan/report/report.py +466 -0
  30. scriptplan/report/report_base.py +397 -0
  31. scriptplan/report/report_context.py +248 -0
  32. scriptplan/report/resource_report.py +341 -0
  33. scriptplan/report/table_report.py +693 -0
  34. scriptplan/report/task_report.py +362 -0
  35. scriptplan/report/text_report.py +172 -0
  36. scriptplan/scheduler/__init__.py +0 -0
  37. scriptplan/scheduler/batch_processor.py +238 -0
  38. scriptplan/scheduler/scoreboard.py +120 -0
  39. scriptplan/utils/__init__.py +0 -0
  40. scriptplan/utils/data_cache.py +46 -0
  41. scriptplan/utils/logger.py +243 -0
  42. scriptplan/utils/message_handler.py +515 -0
  43. scriptplan/utils/time.py +195 -0
  44. scriptplan-0.9.0.dist-info/METADATA +161 -0
  45. scriptplan-0.9.0.dist-info/RECORD +49 -0
  46. scriptplan-0.9.0.dist-info/WHEEL +5 -0
  47. scriptplan-0.9.0.dist-info/entry_points.txt +2 -0
  48. scriptplan-0.9.0.dist-info/licenses/LICENSE +201 -0
  49. 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