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,69 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
class Allocation:
|
|
4
|
+
# Selection modes
|
|
5
|
+
ORDER = 0
|
|
6
|
+
MIN_ALLOCATED = 1
|
|
7
|
+
MIN_LOADED = 2
|
|
8
|
+
MAX_LOADED = 3
|
|
9
|
+
RANDOM = 4
|
|
10
|
+
|
|
11
|
+
def __init__(self, candidates, selectionMode=1, persistent=False, mandatory=False, atomic=False):
|
|
12
|
+
self.candidates_list = candidates
|
|
13
|
+
self.selectionMode = selectionMode
|
|
14
|
+
self.atomic = atomic
|
|
15
|
+
self.persistent = persistent
|
|
16
|
+
self.mandatory = mandatory
|
|
17
|
+
self.shifts = None
|
|
18
|
+
self.lockedResource = None
|
|
19
|
+
self.staticCandidates = None
|
|
20
|
+
|
|
21
|
+
def setSelectionMode(self, mode_str):
|
|
22
|
+
modes = ['order', 'minallocated', 'minloaded', 'maxloaded', 'random']
|
|
23
|
+
try:
|
|
24
|
+
self.selectionMode = modes.index(mode_str)
|
|
25
|
+
except ValueError:
|
|
26
|
+
raise ValueError(f"Unknown selection mode {mode_str}")
|
|
27
|
+
|
|
28
|
+
def addCandidate(self, candidate):
|
|
29
|
+
self.candidates_list.append(candidate)
|
|
30
|
+
|
|
31
|
+
def onShift(self, sbIdx):
|
|
32
|
+
if self.shifts:
|
|
33
|
+
return self.shifts.onShift(sbIdx)
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
def candidates(self, scenarioIdx=None):
|
|
37
|
+
if self.staticCandidates:
|
|
38
|
+
return self.staticCandidates
|
|
39
|
+
|
|
40
|
+
if scenarioIdx is None or self.selectionMode == self.ORDER:
|
|
41
|
+
return self.candidates_list
|
|
42
|
+
|
|
43
|
+
if self.selectionMode == self.RANDOM:
|
|
44
|
+
# Random shuffle
|
|
45
|
+
shuffled = list(self.candidates_list)
|
|
46
|
+
random.shuffle(shuffled)
|
|
47
|
+
return shuffled
|
|
48
|
+
|
|
49
|
+
def sort_key(res):
|
|
50
|
+
if self.selectionMode == self.MIN_ALLOCATED:
|
|
51
|
+
crit = res.get('criticalness', scenarioIdx) or 0.0
|
|
52
|
+
if self.persistent:
|
|
53
|
+
effort = res.bookedEffort(scenarioIdx) or 0
|
|
54
|
+
return (effort, crit)
|
|
55
|
+
else:
|
|
56
|
+
return crit
|
|
57
|
+
elif self.selectionMode == self.MIN_LOADED:
|
|
58
|
+
return res.bookedEffort(scenarioIdx) or 0
|
|
59
|
+
elif self.selectionMode == self.MAX_LOADED:
|
|
60
|
+
return -(res.bookedEffort(scenarioIdx) or 0)
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(f"Unknown selection mode {self.selectionMode}")
|
|
63
|
+
|
|
64
|
+
sorted_list = sorted(self.candidates_list, key=sort_key)
|
|
65
|
+
|
|
66
|
+
if self.selectionMode == self.MIN_ALLOCATED and not self.persistent:
|
|
67
|
+
self.staticCandidates = sorted_list
|
|
68
|
+
|
|
69
|
+
return sorted_list
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from scriptplan.utils.time import TimeInterval
|
|
2
|
+
|
|
3
|
+
class Booking:
|
|
4
|
+
def __init__(self, resource, task, intervals=None):
|
|
5
|
+
self.resource = resource
|
|
6
|
+
self.task = task
|
|
7
|
+
self.intervals = intervals if intervals else []
|
|
8
|
+
self.sourceFileInfo = None
|
|
9
|
+
self.overtime = 0
|
|
10
|
+
self.sloppy = 0
|
|
11
|
+
|
|
12
|
+
def to_s(self):
|
|
13
|
+
out = f"{self.resource.fullId} "
|
|
14
|
+
first = True
|
|
15
|
+
for iv in self.intervals:
|
|
16
|
+
if first:
|
|
17
|
+
first = False
|
|
18
|
+
else:
|
|
19
|
+
out += ", "
|
|
20
|
+
|
|
21
|
+
# Assuming iv.start and iv.end are datetime objects
|
|
22
|
+
duration_hours = (iv.end - iv.start).total_seconds() / 3600
|
|
23
|
+
out += f"{iv.start} + {duration_hours}h"
|
|
24
|
+
return out
|
|
25
|
+
|
|
26
|
+
def to_tjp(self, taskMode):
|
|
27
|
+
out = f"{self.task.fullId} " if taskMode else f"{self.resource.fullId} "
|
|
28
|
+
first = True
|
|
29
|
+
for iv in self.intervals:
|
|
30
|
+
if first:
|
|
31
|
+
first = False
|
|
32
|
+
else:
|
|
33
|
+
out += ",\n"
|
|
34
|
+
|
|
35
|
+
duration_hours = (iv.end - iv.start).total_seconds() / 3600
|
|
36
|
+
out += f"{iv.start} + {duration_hours}h"
|
|
37
|
+
|
|
38
|
+
out += ' { overtime 2 }'
|
|
39
|
+
return out
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Journal - Project journal for tracking status and progress.
|
|
3
|
+
|
|
4
|
+
This module implements the Journal and JournalEntry classes for storing
|
|
5
|
+
and managing status reports and progress updates on tasks and resources.
|
|
6
|
+
|
|
7
|
+
A JournalEntry stores RichText strings to describe a status or property
|
|
8
|
+
of the project at a certain point in time. Additionally, the entry can
|
|
9
|
+
contain a reference to a Resource as author and an alert level.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Optional, List, Any, Dict, Callable
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from enum import IntEnum
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from scriptplan.core.project import Project
|
|
18
|
+
from scriptplan.core.resource import Resource
|
|
19
|
+
from scriptplan.core.task import Task
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AlertLevel(IntEnum):
|
|
23
|
+
"""Alert levels for journal entries."""
|
|
24
|
+
GREEN = 0 # On track
|
|
25
|
+
YELLOW = 1 # Minor issues
|
|
26
|
+
RED = 2 # Major issues/blocked
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class JournalEntry:
|
|
30
|
+
"""
|
|
31
|
+
A journal entry stores status or progress information about a task
|
|
32
|
+
or resource at a specific point in time.
|
|
33
|
+
|
|
34
|
+
The text is structured in 3 elements:
|
|
35
|
+
- headline: A very short description (should not exceed ~40 characters)
|
|
36
|
+
- summary: An introductory or summarizing paragraph (optional)
|
|
37
|
+
- details: A longer text segment (optional)
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
journal: Reference to the parent Journal object
|
|
41
|
+
date: The date of the entry
|
|
42
|
+
headline: Short description (mandatory)
|
|
43
|
+
property: Reference to the Task or Resource this entry is about
|
|
44
|
+
source_file_info: Source file location of this entry
|
|
45
|
+
author: Reference to the Resource who authored this entry
|
|
46
|
+
moderators: List of Resources who moderated this entry
|
|
47
|
+
summary: Introductory/summarizing RichText paragraph
|
|
48
|
+
details: RichText of arbitrary length
|
|
49
|
+
alert_level: The alert level (GREEN, YELLOW, RED)
|
|
50
|
+
flags: List of flag identifiers
|
|
51
|
+
timesheet_record: Reference to associated TimeSheetRecord
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, journal: 'Journal', date: datetime, headline: str,
|
|
55
|
+
property_node: Any, source_file_info: Any = None):
|
|
56
|
+
"""
|
|
57
|
+
Create a new JournalEntry object.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
journal: The parent Journal object
|
|
61
|
+
date: The date of the entry
|
|
62
|
+
headline: Short description text
|
|
63
|
+
property_node: The Task or Resource this entry is about
|
|
64
|
+
source_file_info: Optional source file location
|
|
65
|
+
"""
|
|
66
|
+
self.journal = journal
|
|
67
|
+
self.date = date
|
|
68
|
+
self.headline = headline
|
|
69
|
+
self.property = property_node
|
|
70
|
+
self.source_file_info = source_file_info
|
|
71
|
+
|
|
72
|
+
self.author: Optional['Resource'] = None
|
|
73
|
+
self.moderators: List['Resource'] = []
|
|
74
|
+
self.summary: Optional[str] = None
|
|
75
|
+
self.details: Optional[str] = None
|
|
76
|
+
self.alert_level: AlertLevel = AlertLevel.GREEN
|
|
77
|
+
self.flags: List[str] = []
|
|
78
|
+
self.timesheet_record: Any = None
|
|
79
|
+
|
|
80
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
81
|
+
"""Convert entry to dictionary for serialization."""
|
|
82
|
+
return {
|
|
83
|
+
'date': self.date.isoformat() if self.date else None,
|
|
84
|
+
'headline': self.headline,
|
|
85
|
+
'property_id': self.property.fullId if self.property else None,
|
|
86
|
+
'author_id': self.author.fullId if self.author else None,
|
|
87
|
+
'alert_level': self.alert_level.name,
|
|
88
|
+
'summary': str(self.summary) if self.summary else None,
|
|
89
|
+
'details': str(self.details) if self.details else None,
|
|
90
|
+
'flags': self.flags,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def __repr__(self) -> str:
|
|
94
|
+
return (f"JournalEntry(date={self.date}, headline='{self.headline}', "
|
|
95
|
+
f"property={self.property.fullId if self.property else None})")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class JournalEntryList(list):
|
|
99
|
+
"""
|
|
100
|
+
A list of JournalEntry objects with sorting capabilities.
|
|
101
|
+
|
|
102
|
+
This class provides methods to sort journal entries and apply
|
|
103
|
+
various filtering operations.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, entries: Optional[List[JournalEntry]] = None):
|
|
107
|
+
"""Initialize with optional list of entries."""
|
|
108
|
+
super().__init__(entries or [])
|
|
109
|
+
|
|
110
|
+
def sort_by(self, criteria: List[tuple]) -> 'JournalEntryList':
|
|
111
|
+
"""
|
|
112
|
+
Sort entries by multiple criteria.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
criteria: List of (attribute, ascending) tuples
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Self for chaining
|
|
119
|
+
"""
|
|
120
|
+
def sort_key(entry: JournalEntry):
|
|
121
|
+
key_parts = []
|
|
122
|
+
for attr, ascending in criteria:
|
|
123
|
+
val = getattr(entry, attr, None)
|
|
124
|
+
if val is None:
|
|
125
|
+
val = ''
|
|
126
|
+
if not ascending:
|
|
127
|
+
if isinstance(val, (int, float)):
|
|
128
|
+
val = -val
|
|
129
|
+
elif isinstance(val, datetime):
|
|
130
|
+
# Invert datetime for descending
|
|
131
|
+
val = datetime.max - val
|
|
132
|
+
key_parts.append(val)
|
|
133
|
+
return tuple(key_parts)
|
|
134
|
+
|
|
135
|
+
self.sort(key=sort_key)
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
def filter(self, predicate: Callable[[JournalEntry], bool]) -> 'JournalEntryList':
|
|
139
|
+
"""
|
|
140
|
+
Filter entries using a predicate function.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
predicate: Function that returns True for entries to keep
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
New JournalEntryList with filtered entries
|
|
147
|
+
"""
|
|
148
|
+
return JournalEntryList([e for e in self if predicate(e)])
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class Journal:
|
|
152
|
+
"""
|
|
153
|
+
Container for all JournalEntry objects of a project.
|
|
154
|
+
|
|
155
|
+
The Journal provides methods to add entries and query them by
|
|
156
|
+
task, resource, date range, and other criteria.
|
|
157
|
+
|
|
158
|
+
Attributes:
|
|
159
|
+
project: Reference to the Project object
|
|
160
|
+
entries: List of all journal entries
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def __init__(self, project: Optional['Project'] = None):
|
|
164
|
+
"""
|
|
165
|
+
Create a new Journal.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
project: Optional reference to the Project
|
|
169
|
+
"""
|
|
170
|
+
self.project = project
|
|
171
|
+
self._entries: List[JournalEntry] = []
|
|
172
|
+
self._entries_by_property: Dict[str, List[JournalEntry]] = {}
|
|
173
|
+
|
|
174
|
+
def add_entry(self, entry: JournalEntry) -> JournalEntry:
|
|
175
|
+
"""
|
|
176
|
+
Add a journal entry to the journal.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
entry: The JournalEntry to add
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
The added entry
|
|
183
|
+
"""
|
|
184
|
+
self._entries.append(entry)
|
|
185
|
+
|
|
186
|
+
# Index by property ID for fast lookup
|
|
187
|
+
if entry.property:
|
|
188
|
+
prop_id = entry.property.fullId
|
|
189
|
+
if prop_id not in self._entries_by_property:
|
|
190
|
+
self._entries_by_property[prop_id] = []
|
|
191
|
+
self._entries_by_property[prop_id].append(entry)
|
|
192
|
+
|
|
193
|
+
return entry
|
|
194
|
+
|
|
195
|
+
def create_entry(self, date: datetime, headline: str, property_node: Any,
|
|
196
|
+
source_file_info: Any = None) -> JournalEntry:
|
|
197
|
+
"""
|
|
198
|
+
Create and add a new journal entry.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
date: The date of the entry
|
|
202
|
+
headline: Short description text
|
|
203
|
+
property_node: The Task or Resource this entry is about
|
|
204
|
+
source_file_info: Optional source file location
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
The created JournalEntry
|
|
208
|
+
"""
|
|
209
|
+
entry = JournalEntry(self, date, headline, property_node, source_file_info)
|
|
210
|
+
return self.add_entry(entry)
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def entries(self) -> JournalEntryList:
|
|
214
|
+
"""Get all entries as a JournalEntryList."""
|
|
215
|
+
return JournalEntryList(self._entries)
|
|
216
|
+
|
|
217
|
+
def __len__(self) -> int:
|
|
218
|
+
"""Return the number of entries."""
|
|
219
|
+
return len(self._entries)
|
|
220
|
+
|
|
221
|
+
def __iter__(self):
|
|
222
|
+
"""Iterate over entries."""
|
|
223
|
+
return iter(self._entries)
|
|
224
|
+
|
|
225
|
+
def entries_by_task(self, task: 'Task', start: Optional[datetime] = None,
|
|
226
|
+
end: Optional[datetime] = None,
|
|
227
|
+
alert_level: Optional[AlertLevel] = None) -> JournalEntryList:
|
|
228
|
+
"""
|
|
229
|
+
Get journal entries for a specific task.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
task: The task to get entries for
|
|
233
|
+
start: Optional start date filter
|
|
234
|
+
end: Optional end date filter
|
|
235
|
+
alert_level: Optional minimum alert level filter
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
JournalEntryList of matching entries
|
|
239
|
+
"""
|
|
240
|
+
entries = self._entries_by_property.get(task.fullId, [])
|
|
241
|
+
return self._filter_entries(entries, start, end, alert_level)
|
|
242
|
+
|
|
243
|
+
def entries_by_task_recursive(self, task: 'Task', start: Optional[datetime] = None,
|
|
244
|
+
end: Optional[datetime] = None,
|
|
245
|
+
alert_level: Optional[AlertLevel] = None) -> JournalEntryList:
|
|
246
|
+
"""
|
|
247
|
+
Get journal entries for a task and all its children.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
task: The root task
|
|
251
|
+
start: Optional start date filter
|
|
252
|
+
end: Optional end date filter
|
|
253
|
+
alert_level: Optional minimum alert level filter
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
JournalEntryList of matching entries from task and children
|
|
257
|
+
"""
|
|
258
|
+
result = []
|
|
259
|
+
|
|
260
|
+
# Get entries for this task
|
|
261
|
+
result.extend(self._entries_by_property.get(task.fullId, []))
|
|
262
|
+
|
|
263
|
+
# Get entries for all children recursively
|
|
264
|
+
if hasattr(task, 'children'):
|
|
265
|
+
for child in task.children:
|
|
266
|
+
result.extend(self.entries_by_task_recursive(child, start, end, alert_level))
|
|
267
|
+
|
|
268
|
+
return self._filter_entries(result, start, end, alert_level)
|
|
269
|
+
|
|
270
|
+
def entries_by_resource(self, resource: 'Resource',
|
|
271
|
+
start: Optional[datetime] = None,
|
|
272
|
+
end: Optional[datetime] = None) -> JournalEntryList:
|
|
273
|
+
"""
|
|
274
|
+
Get journal entries authored by a specific resource.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
resource: The author resource
|
|
278
|
+
start: Optional start date filter
|
|
279
|
+
end: Optional end date filter
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
JournalEntryList of entries by this resource
|
|
283
|
+
"""
|
|
284
|
+
entries = [e for e in self._entries if e.author == resource]
|
|
285
|
+
return self._filter_entries(entries, start, end)
|
|
286
|
+
|
|
287
|
+
def entries_by_date(self, date: datetime) -> JournalEntryList:
|
|
288
|
+
"""
|
|
289
|
+
Get all journal entries for a specific date.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
date: The date to query
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
JournalEntryList of entries on that date
|
|
296
|
+
"""
|
|
297
|
+
# Compare dates only (ignore time)
|
|
298
|
+
target_date = date.date() if isinstance(date, datetime) else date
|
|
299
|
+
entries = [e for e in self._entries
|
|
300
|
+
if e.date and e.date.date() == target_date]
|
|
301
|
+
return JournalEntryList(entries)
|
|
302
|
+
|
|
303
|
+
def entries_in_range(self, start: datetime, end: datetime) -> JournalEntryList:
|
|
304
|
+
"""
|
|
305
|
+
Get all journal entries within a date range.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
start: Start date (inclusive)
|
|
309
|
+
end: End date (exclusive)
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
JournalEntryList of entries in the range
|
|
313
|
+
"""
|
|
314
|
+
return self._filter_entries(self._entries, start, end)
|
|
315
|
+
|
|
316
|
+
def current_entries(self, scenario_idx: int, property_node: Any,
|
|
317
|
+
start: datetime, end: datetime,
|
|
318
|
+
alert_level: Optional[AlertLevel] = None) -> JournalEntryList:
|
|
319
|
+
"""
|
|
320
|
+
Get current (most recent) entries for a property within a time range.
|
|
321
|
+
|
|
322
|
+
This is used to get the latest status for tasks/resources.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
scenario_idx: Scenario index
|
|
326
|
+
property_node: The task or resource
|
|
327
|
+
start: Start of time range
|
|
328
|
+
end: End of time range
|
|
329
|
+
alert_level: Optional minimum alert level
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
JournalEntryList of current entries
|
|
333
|
+
"""
|
|
334
|
+
entries = self._entries_by_property.get(property_node.fullId, [])
|
|
335
|
+
filtered = self._filter_entries(entries, start, end, alert_level)
|
|
336
|
+
|
|
337
|
+
# Sort by date descending and return most recent
|
|
338
|
+
filtered.sort_by([('date', False)])
|
|
339
|
+
return filtered
|
|
340
|
+
|
|
341
|
+
def _filter_entries(self, entries: List[JournalEntry],
|
|
342
|
+
start: Optional[datetime] = None,
|
|
343
|
+
end: Optional[datetime] = None,
|
|
344
|
+
alert_level: Optional[AlertLevel] = None) -> JournalEntryList:
|
|
345
|
+
"""
|
|
346
|
+
Apply date and alert level filters to entries.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
entries: List of entries to filter
|
|
350
|
+
start: Optional start date (inclusive)
|
|
351
|
+
end: Optional end date (exclusive)
|
|
352
|
+
alert_level: Optional minimum alert level
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Filtered JournalEntryList
|
|
356
|
+
"""
|
|
357
|
+
result = list(entries)
|
|
358
|
+
|
|
359
|
+
if start:
|
|
360
|
+
result = [e for e in result if e.date and e.date >= start]
|
|
361
|
+
|
|
362
|
+
if end:
|
|
363
|
+
result = [e for e in result if e.date and e.date < end]
|
|
364
|
+
|
|
365
|
+
if alert_level is not None:
|
|
366
|
+
result = [e for e in result if e.alert_level >= alert_level]
|
|
367
|
+
|
|
368
|
+
return JournalEntryList(result)
|
|
369
|
+
|
|
370
|
+
def clear(self) -> None:
|
|
371
|
+
"""Remove all entries from the journal."""
|
|
372
|
+
self._entries.clear()
|
|
373
|
+
self._entries_by_property.clear()
|
|
374
|
+
|
|
375
|
+
def to_list(self) -> List[Dict[str, Any]]:
|
|
376
|
+
"""Convert all entries to a list of dictionaries."""
|
|
377
|
+
return [entry.to_dict() for entry in self._entries]
|
scriptplan/core/leave.py
ADDED