From 4efa1668b33ff1ed518ab1b54ab877f147d0ca70 Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Tue, 5 Dec 2023 13:35:09 -0500 Subject: [PATCH 01/47] documented alarm_checker.py --- src/astra/usecase/alarm_checker.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/astra/usecase/alarm_checker.py b/src/astra/usecase/alarm_checker.py index 2325f34..1b2e9be 100644 --- a/src/astra/usecase/alarm_checker.py +++ b/src/astra/usecase/alarm_checker.py @@ -7,14 +7,18 @@ def check_alarms(dm: DataManager, earliest_time: datetime) -> None: """ - Goes through all possible alarms to check and, if any exists, adds them to - based on their criticality + Goes through all possible alarm bases to check, and calls the appropriate + strategy to evaluate them - :param earliest_time: - :param dm: The manager of all data known to the program + This should only be called once new telemetry data is added to the system + + :param dm: Contains all data known to the program + :param earliest_time: Details the earliest timestamp amongst newly added telemetry frames """ alarm_bases = dm.alarm_bases + + # condition variable allows us to make a notification once all strategies have completed cv = Condition() threads = [] @@ -22,8 +26,10 @@ def check_alarms(dm: DataManager, base = alarm_base.event_base criticality = alarm_base.criticality + # Acquiring the correct strategy for the event base then running it in a new thread strategy = get_strategy(base) new_thread = Thread(target=strategy, args=[dm, base, criticality, earliest_time, False, cv]) + new_thread.start() threads.append(new_thread) wait_for_children(dm, cv, threads) @@ -31,16 +37,18 @@ def check_alarms(dm: DataManager, def wait_for_children(dm: DataManager, cv: Condition, threads: list[Thread]) -> None: """ - Waits for all child threads to be completed, then notifies the alarm container that - all tasks are done + Waits for all threads in to be completed, then notifies the alarm container that + all alarm bases have been evaluated - :param dm: Container of the alarm container - :param cv: The condition variable to track child completion + :param dm: Contains all data known to the program + :param cv: The condition variable each thread in has to track thread completion :param threads: Contains all child threads of this process """ + thread_active = check_alive(threads) with cv: + # Simple loop to check if any child thread is alive and block if so while thread_active: # Note: this idea might be doable with a semaphore? My only concern is if the child # threads don't acquire a semaphore before this thread, then it won't work @@ -51,10 +59,11 @@ def wait_for_children(dm: DataManager, cv: Condition, threads: list[Thread]) -> def check_alive(threads: list[Thread]) -> bool: """ - Checks if any thread in is alive and returns an appropriate boolean + Checks if any thread in is alive and returns an appropriate boolean :param threads: The list of threads to check """ + thread_active = False for thread in threads: if thread.is_alive(): From 237f28d1f9716388498352e126f1b92d409c1bcf Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Tue, 5 Dec 2023 14:34:01 -0500 Subject: [PATCH 02/47] documented alarm handler and made some minor changes --- src/astra/data/alarm_container.py | 11 +- src/astra/usecase/alarm_checker.py | 2 + src/astra/usecase/alarm_handler.py | 175 +++++++++++-------- src/astra/usecase/alarms_request_receiver.py | 2 +- 4 files changed, 109 insertions(+), 81 deletions(-) diff --git a/src/astra/data/alarm_container.py b/src/astra/data/alarm_container.py index a2f08f4..8096496 100644 --- a/src/astra/data/alarm_container.py +++ b/src/astra/data/alarm_container.py @@ -5,8 +5,6 @@ from astra.data.alarms import AlarmPriority, Alarm, AlarmCriticality -NEW_QUEUE_KEY = 'n' - class AlarmObserver: """ @@ -49,11 +47,12 @@ class AlarmsContainer: observer = AlarmObserver() alarms = {AlarmPriority.WARNING.name: [], AlarmPriority.LOW.name: [], AlarmPriority.MEDIUM.name: [], AlarmPriority.HIGH.name: [], - AlarmPriority.CRITICAL.name: [], NEW_QUEUE_KEY: Queue()} + AlarmPriority.CRITICAL.name: []} + new_alarms = Queue() mutex = Lock() @classmethod - def get_alarms(cls) -> dict[str, list[Alarm] | Queue]: + def get_alarms(cls) -> dict[str, list[Alarm]]: """ Returns a shallow copy of @@ -83,7 +82,7 @@ def add_alarms(cls, alarms: list[Alarm], for alarm in alarms: criticality = alarm.criticality alarm_timer_vals = [] - cls.alarms[NEW_QUEUE_KEY].put(alarm) + cls.new_alarms.put(alarm) # Find the closest timeframe from 0, 5, 15, and 30 minutes from when the # alarm was created to when it was actually confirmed @@ -179,7 +178,7 @@ def remove_alarm(cls, alarm: Alarm) -> None: """ with cls.mutex: for priority in cls.alarms: - if priority != NEW_QUEUE_KEY and alarm in cls.alarms[priority]: + if alarm in cls.alarms[priority]: # Note: We don't consider the queue of new alarms, since by the time # the alarm can be removed, it's already be taken out of the queue cls.alarms[priority].remove(alarm) diff --git a/src/astra/usecase/alarm_checker.py b/src/astra/usecase/alarm_checker.py index 1b2e9be..fa7d488 100644 --- a/src/astra/usecase/alarm_checker.py +++ b/src/astra/usecase/alarm_checker.py @@ -23,6 +23,8 @@ def check_alarms(dm: DataManager, threads = [] for alarm_base in alarm_bases: + # The base and criticality were seperated due to how things worked previously. This would + # be a good and simple thing to refactor base = alarm_base.event_base criticality = alarm_base.criticality diff --git a/src/astra/usecase/alarm_handler.py b/src/astra/usecase/alarm_handler.py index ee5a429..88ca6c0 100644 --- a/src/astra/usecase/alarm_handler.py +++ b/src/astra/usecase/alarm_handler.py @@ -21,24 +21,30 @@ OLD_QUEUE_KEY = 'o' NEW_PREFIX = "[NEW] " MAX_BANNER_SIZE = 6 +MAX_NEW_SIZE = 3 +MAX_OLD_SIZE = 3 -class LimitedSlotAlarms: +class AlarmBanner: """ - Contains all alarms to be shown in the banner at the top of the screen + Stores alarms to be shown in the banner at the top of the screen according to acknowledgement + + :param _slots: Maps constant strings to the appropriate alarms list + :type: dict[str, list[Alarm]] - :param _slots: A dict of priority queues :param _priorities: an ordered list of keys in _slots, where elements are ordered by descending importance + :type: list[str] """ - _slots = {NEW_QUEUE_KEY: [], OLD_QUEUE_KEY: []} + + _slots = {NEW_QUEUE_KEY: [], OLD_QUEUE_KEY: []} # type: dict[str, list[Alarm]] _priorities = [NEW_QUEUE_KEY, OLD_QUEUE_KEY] @staticmethod def create_banner_string(alarm: Alarm) -> str: """Creates an appropriate string for the banner provided an alarm - :param alarm: The alarm to use in creating a string + :param alarm: The alarm to detail in the returned string :return A representation of the alarm for the banner """ priority_str = (f"{alarm.priority.name[0] + alarm.priority.name[1:].lower()}" @@ -50,14 +56,21 @@ def create_banner_string(alarm: Alarm) -> str: @classmethod def get_all(cls) -> list[str]: """ - Compacts all data amongst into one list in a readable format + Determines which alarms should be shown in the banners and returns an ordered list + of strings to display - :return: An ordered list of data compiled from + Currently, uses the following criteria to determine what should be shown: + 1. 3 new alarms and 3 old alarms should be shown. If there are less than 3 of either, + the other may take its place + 2. In each age bracket, we use the highest priority to determine which should be shown + + :return: An ordered list of strings to show in the banner """ - all_items = [] + + all_items: list[str] = [] # Note: while this implementation may seem slow, due to the nature of alarms - # being rare and banner slots being limited, it's effectively fast + # being rare and banner slots being limited, speed should not be an issue new_q = cls._slots[NEW_QUEUE_KEY] new_q_items = new_q.copy() new_q_items.sort(reverse=True) @@ -66,10 +79,10 @@ def get_all(cls) -> list[str]: # We want to reserve 3 slots for old alarms, but if there's less than 3 old alarms, # populate the banner with more new ones old_q_size = len(old_q) - if old_q_size > 3: - max_new = 3 + if old_q_size > MAX_NEW_SIZE: + max_new = MAX_NEW_SIZE else: - max_new = 6 - old_q_size + max_new = MAX_BANNER_SIZE - old_q_size # Getting and formatting strings for the top priority new alarms i = 0 @@ -83,7 +96,10 @@ def get_all(cls) -> list[str]: i = 0 old_q_items = old_q.copy() old_q_items.sort(reverse=True) - while i < len(old_q_items) and len(all_items) < MAX_BANNER_SIZE: + + # We run a while loop over the size of to ensure the + # banner is as populated as possible + while len(all_items) < MAX_BANNER_SIZE: item = old_q_items[i] old_str = cls.create_banner_string(item) all_items.append(old_str) @@ -96,7 +112,7 @@ def insert_into_new(cls, alarm: Alarm) -> None: """ Inserts into the new alarms queue in - :param alarm: The alarm to insert into the banner slots + :param alarm: The alarm to insert into the new alarms queue """ new_q = cls._slots[NEW_QUEUE_KEY] new_q.append(alarm) @@ -104,9 +120,9 @@ def insert_into_new(cls, alarm: Alarm) -> None: @classmethod def insert_into_old(cls, alarm: Alarm) -> None: """ - Moves from the new alarms queue and into the old alarms queue + Moves from the new alarms list and into the old alarms list - :param alarm: The alarm to insert into the banner slots + :param alarm: The alarm to insert into the old banner slots PRECONDITION: is in """ @@ -121,9 +137,9 @@ def remove_alarm_from_banner(cls, alarm: Alarm) -> None: """ Removes from the appropriate queue - :param alarm: The alarm to insert into the banner slots + :param alarm: The alarm to remove from any banner slots - PRECONDITION: is in one queue in + PRECONDITION: is in one list in """ if alarm.acknowledged: old_q = cls._slots[OLD_QUEUE_KEY] @@ -134,16 +150,28 @@ def remove_alarm_from_banner(cls, alarm: Alarm) -> None: class AlarmsHandler: - banner_container = LimitedSlotAlarms() + """ + Processes requests of the program when it comes to displaying alarm information + + :param banner_container: Data structure to determine what information to store in the + alarm banner at the top of the screen + :type: AlarmBanner + """ + + banner_container = AlarmBanner() @staticmethod def _get_alarm_type(alarm: Alarm) -> str: """ Returns the string representation of the underlying EventBase of - :param alarm: The alarm to examine + NOTE: This is a pretty bad code smell. It will work--and if there are no further alarm + types to add then this is fine enough--but this should be refactored. + + :param alarm: The alarm to extract a type from :return: A string representation of the alarm type """ + base = alarm.event.base match base: case RateOfChangeEventBase(): @@ -161,24 +189,6 @@ def _get_alarm_type(alarm: Alarm) -> str: case _: return 'L_OR' - @classmethod - def _get_relevant_tags(cls, event_base: EventBase) -> set[Tag]: - """ - Takes an event base and outputs a list of all relevant tags to the base - - :param event_base: The event base to examine - :return: A list of all relevant tags to the base - """ - if type(event_base) is not AllEventBase \ - and type(event_base) is not AnyEventBase \ - and type(event_base) is not SOEEventBase: - return {event_base.tag} - else: - all_tags = set() - for inner_event_base in event_base.event_bases: - all_tags = all_tags.union(cls._get_relevant_tags(inner_event_base)) - return all_tags - @classmethod def _determine_toggled(cls, alarm: Alarm, filter_args: AlarmsFilters) -> bool: """ @@ -188,19 +198,25 @@ def _determine_toggled(cls, alarm: Alarm, filter_args: AlarmsFilters) -> bool: :param filter_args: Contains arguments that will determine if is shown :return: true iff should be shown """ - show = True + # First, checking if it satisfies priority requirements - show = alarm.priority.name in filter_args.priorities + + show: bool = True + if filter_args.priorities is not None: + show = alarm.priority.name in filter_args.priorities # Next, checking if it satisfies criticality arguments - show = show and alarm.criticality.name in filter_args.criticalities + if filter_args.criticalities is not None: + show = show and alarm.criticality.name in filter_args.criticalities # Checking if the alarm type matches - show = show and cls._get_alarm_type(alarm) in filter_args.types + if filter_args.types is not None: + show = show and cls._get_alarm_type(alarm) in filter_args.types # Checking if the tag of the alarm is requested to be shown - relevant_tags = set(cls._get_relevant_tags(alarm.event.base)) - show = show and len(relevant_tags.difference(filter_args.tags)) == 0 + if filter_args.tags is not None: + relevant_tags = set(alarm.event.base.tags) + show = show and len(relevant_tags.difference(filter_args.tags)) == 0 # Now we need to make sure the alarm fits in the time parameters alarm_confirm_time = alarm.event.confirm_time @@ -232,28 +248,27 @@ def _sort_output(cls, return_data: TableReturn, sort: tuple[str, str]): sorts the field of return_data based on :param return_data: the output container to sort data from - :param sort: defines how output should be sorted - - PRECONDITION: The values of sort satisfy the docstrings of the sort field in - AlarmsFilters + :param sort: indicates what type of sort should be applied to which column. + A tuple in the form (sort_type, sort_column), where is one + of '>' or '<', and is in VALID_SORTING_COLUMNS. """ - if sort is not None: - # Determining which column to sort by - key_index = 0 - for i in range(len(VALID_SORTING_COLUMNS)): - if sort[1] == VALID_SORTING_COLUMNS[i]: - key_index = i - break - - # By default, sorting occurs by ascending values, so a case is - # needed to check if it should occur by descending order - reverse = False - if sort[0] == DESCENDING: - reverse = True - - return_data.table = sorted(return_data.table, - key=lambda x: (x[key_index], x[0]), - reverse=reverse) + + # Determining which column to sort by + key_index = 0 + for i in range(len(VALID_SORTING_COLUMNS)): + if sort[1] == VALID_SORTING_COLUMNS[i]: + key_index = i + break + + # By default, sorting occurs by ascending values, so a case is + # needed to check if it should occur by descending order + reverse = False + if sort[0] == DESCENDING: + reverse = True + + return_data.table = sorted(return_data.table, + key=lambda x: (x[key_index], x[0]), + reverse=reverse) @classmethod def _extract_alarm_data(cls, alarm: Alarm, priority: AlarmPriority) -> list: @@ -266,13 +281,14 @@ def _extract_alarm_data(cls, alarm: Alarm, priority: AlarmPriority) -> list: register time, confirm time, type, description, and the alarm itself """ + alarm_id = alarm.event.id alarm_priority = priority alarm_criticality = alarm.criticality alarm_register_time = alarm.event.register_time alarm_confirm_time = alarm.event.confirm_time alarm_type = cls._get_alarm_type(alarm) - alarm_tags = cls._get_relevant_tags(alarm.event.base) + alarm_tags = alarm.event.base.tags tag_string = '' for tag in alarm_tags: tag_string += tag + ' ' @@ -284,41 +300,50 @@ def _extract_alarm_data(cls, alarm: Alarm, priority: AlarmPriority) -> list: return new_row @classmethod - def get_data(cls, dm: dict[AlarmPriority | str, set[Alarm] | Queue], + def get_data(cls, dm: DataManager, filter_args: AlarmsFilters) -> TableReturn: """ Using the current data structure of alarms, packs all data stored by the alarms into an easily accessible format - :param dm: Contains all alarms known to the program + :param dm: Contains all data known to the program :param filter_args: Describes all filters to apply to the filter :return: A container for all data to be shown by the table """ shown = [] removed = [] + + alarms_container = dm.alarms + alarms = alarms_container.get_alarms() + + # simply iterating through all alarms and extracting their data then adding it into + # the appropriate list from those defined above for priority in PRIORITIES: - for alarm in dm[priority]: + for alarm in alarms[priority]: new_row = cls._extract_alarm_data(alarm, AlarmPriority(priority)) if cls._determine_toggled(alarm, filter_args): shown.append(new_row) else: removed.append(new_row) - while dm[NEW_QUEUE_KEY].qsize() > 0: - next_item = dm[NEW_QUEUE_KEY].get() + # emptying the queue of new alarms to inform the alarm banner of their presence + while alarms_container.new_alarms.qsize() > 0: + next_item = alarms_container.new_alarms.get() cls.banner_container.insert_into_new(next_item) return_table = TableReturn(shown, removed) + if filter_args.sort is not None: + cls._sort_output(return_table, filter_args.sort) + return return_table @classmethod def update_data(cls, prev_data: TableReturn, filter_args: AlarmsFilters) -> None: """ - Updates the previous data returned by get_data to apply any new filters + Updates the previous data returned by get_data to apply any new filters or sort :param prev_data: The data returned by the last call to cls.get_data :param filter_args: Describes all filters to apply to the table - :param dm: Contains all data known to the program """ new_table = [] new_removed = [] @@ -338,7 +363,9 @@ def update_data(cls, prev_data: TableReturn, filter_args: AlarmsFilters) -> None prev_data.table = new_table prev_data.removed = new_removed - cls._sort_output(prev_data, filter_args.sort) + + if filter_args.sort is not None: + cls._sort_output(prev_data, filter_args.sort) @classmethod def acknowledge_alarm(cls, alarm: Alarm, dm: DataManager) -> None: diff --git a/src/astra/usecase/alarms_request_receiver.py b/src/astra/usecase/alarms_request_receiver.py index d4c0f33..2f22abf 100644 --- a/src/astra/usecase/alarms_request_receiver.py +++ b/src/astra/usecase/alarms_request_receiver.py @@ -61,7 +61,7 @@ def create(cls, dm: DataManager) -> TableReturn: cls.filters.tags = set(dm.tags) # Create the initial table. - cls.previous_data = cls.handler.get_data(dm.alarms.get_alarms(), cls.filters) + cls.previous_data = cls.handler.get_data(dm, cls.filters) return cls.previous_data @classmethod From a5bba92d3ce5ed0fc3920accecba46b942e74f40 Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Tue, 5 Dec 2023 14:34:52 -0500 Subject: [PATCH 03/47] minor style fix --- src/astra/usecase/alarm_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/astra/usecase/alarm_handler.py b/src/astra/usecase/alarm_handler.py index 88ca6c0..8c8c72e 100644 --- a/src/astra/usecase/alarm_handler.py +++ b/src/astra/usecase/alarm_handler.py @@ -267,8 +267,8 @@ def _sort_output(cls, return_data: TableReturn, sort: tuple[str, str]): reverse = True return_data.table = sorted(return_data.table, - key=lambda x: (x[key_index], x[0]), - reverse=reverse) + key=lambda x: (x[key_index], x[0]), + reverse=reverse) @classmethod def _extract_alarm_data(cls, alarm: Alarm, priority: AlarmPriority) -> list: From fe3513d179350ed163909652f8dfa72b1ed73bc2 Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Tue, 5 Dec 2023 14:38:24 -0500 Subject: [PATCH 04/47] flake8 fixes --- src/astra/usecase/alarm_handler.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/astra/usecase/alarm_handler.py b/src/astra/usecase/alarm_handler.py index 8c8c72e..b716c1c 100644 --- a/src/astra/usecase/alarm_handler.py +++ b/src/astra/usecase/alarm_handler.py @@ -1,10 +1,7 @@ -from queue import Queue - from astra.data.alarms import (AlarmPriority, RateOfChangeEventBase, Alarm, StaticEventBase, ThresholdEventBase, SetpointEventBase, - SOEEventBase, AllEventBase, EventBase, AnyEventBase) + SOEEventBase, AllEventBase) from astra.data.data_manager import DataManager -from astra.data.parameters import Tag from astra.usecase.filters import AlarmsFilters from astra.usecase.table_return import TableReturn From 109ffea628bf8c485c987f92bb874162c6f435ae Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Tue, 5 Dec 2023 14:49:01 -0500 Subject: [PATCH 05/47] minor changes to default values on filters --- src/astra/usecase/alarms_request_receiver.py | 4 +-- src/astra/usecase/filters.py | 25 +++++++++---------- src/astra/usecase/graphing_handler.py | 3 +-- .../usecase/graphing_request_receiver.py | 11 ++++---- src/astra/usecase/request_receiver.py | 2 +- 5 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/astra/usecase/alarms_request_receiver.py b/src/astra/usecase/alarms_request_receiver.py index 2f22abf..e179407 100644 --- a/src/astra/usecase/alarms_request_receiver.py +++ b/src/astra/usecase/alarms_request_receiver.py @@ -37,8 +37,8 @@ class AlarmsRequestReceiver(RequestReceiver): from the sets of them that we are viewing, and updating the sorting filter to be applied. """ - filters = AlarmsFilters(None, None, CRITICALITIES, PRIORITIES, ALL_TYPES, None, None, None, - None, False) + filters = AlarmsFilters(None, None, CRITICALITIES, PRIORITIES, ALL_TYPES, datetime.min, + datetime.max, datetime.min, datetime.max, False) handler = AlarmsHandler() previous_data = None _sorting = [-1, 1, 1, 1, 1, 1] diff --git a/src/astra/usecase/filters.py b/src/astra/usecase/filters.py index f75f402..6ad9065 100644 --- a/src/astra/usecase/filters.py +++ b/src/astra/usecase/filters.py @@ -32,9 +32,9 @@ class DashboardFilters(Filters): """ sort: tuple[str, str] | None - index: int | None - start_time: datetime | None - end_time: datetime | None + index: int + start_time: datetime + end_time: datetime @dataclass @@ -61,13 +61,13 @@ class AlarmsFilters(Filters): """ sort: tuple[str, str] | None - priorities: set[AlarmPriority] | None - criticalities: set[AlarmCriticality] | None - types: set[str] | None - registered_start_time: datetime | None - registered_end_time: datetime | None - confirmed_start_time: datetime | None - confirmed_end_time: datetime | None + priorities: set[str] + criticalities: set[str] + types: set[str] + registered_start_time: datetime + registered_end_time: datetime + confirmed_start_time: datetime + confirmed_end_time: datetime new: bool @@ -78,8 +78,7 @@ class GraphingFilters(Filters): :param start_time: the earliest time that values for each tag are from. :param end_time: the latest time that values for each tag are from. - :param interval: The number of frams between each value in the list of values. """ - start_time: datetime | None - end_time: datetime | None + start_time: datetime + end_time: datetime diff --git a/src/astra/usecase/graphing_handler.py b/src/astra/usecase/graphing_handler.py index 806f2ae..197b1a0 100644 --- a/src/astra/usecase/graphing_handler.py +++ b/src/astra/usecase/graphing_handler.py @@ -63,7 +63,6 @@ def update_data(cls, prev_data: GraphingData, filter_args: GraphingFilters) -> N :param prev_data: The representation of the current state of displayed data :param filter_args: Contains all information on filters to be applied - :param dm: Contains all data stored by the program to date """ cls._filter_graphing_data(prev_data, filter_args) @@ -126,7 +125,7 @@ def _filter_times(cls, times_list: list[datetime], This method returns a tuple of ints representing indices that give a slice of the where all times in it are >= and <= . - :param imtes_list: The list of times in chronological order that we need the slice of, which + :param times_list: The list of times in chronological order that we need the slice of, which is between the and . :param start_time: The datetime that is the earliest time that values for each tag are from. diff --git a/src/astra/usecase/graphing_request_receiver.py b/src/astra/usecase/graphing_request_receiver.py index 10fa322..9922ab8 100644 --- a/src/astra/usecase/graphing_request_receiver.py +++ b/src/astra/usecase/graphing_request_receiver.py @@ -16,7 +16,7 @@ class GraphingRequestReceiver(RequestReceiver): """ handler = GraphingHandler() - filters = GraphingFilters(set(), None, None) + filters = GraphingFilters(set(), datetime.min, datetime.max) @classmethod def create(cls, dm: DataManager) -> GraphingData: @@ -42,7 +42,7 @@ def set_start_date(cls, start_date: datetime) -> None: """ Sets the start date of the graph by updating the filters. - :param start_time: The new start date of the graph. + :param start_date: The new start date of the graph. """ cls.filters.start_time = start_date @@ -52,7 +52,7 @@ def set_end_date(cls, end_date: datetime) -> None: """ Sets the end date of the graph by updating the filters. - :param end_time: The new end date of the graph. + :param end_date: The new end date of the graph. """ cls.filters.end_time = end_date @@ -74,7 +74,7 @@ def remove_shown_tag(cls, tag: str) -> None: """ Removes a tag from the set of shown tags by updating the filters. - :param tags: The name of the tag to be removed. + :param tag: The name of the tag to be removed. """ if tag in cls.filters.tags: @@ -85,7 +85,7 @@ def add_shown_tag(cls, tag: str) -> None: """ Adds a tag to the set of shown tags by updating the filters. - :param tags: The name of the tag to be added. + :param tag: The name of the tag to be added. """ cls.filters.tags.add(Tag(tag)) @@ -97,7 +97,6 @@ def export_data_to_file(cls, filename: str) -> None: :param filename: The path to save to. The export format is determined based on the file extension. - :param data: The data to be exported. """ cls.handler.export_data_to_file(cls.previous_data, filename) diff --git a/src/astra/usecase/request_receiver.py b/src/astra/usecase/request_receiver.py index c0d0345..5e0f0fb 100644 --- a/src/astra/usecase/request_receiver.py +++ b/src/astra/usecase/request_receiver.py @@ -115,7 +115,7 @@ class DashboardRequestReceiver(RequestReceiver): and updating the sorting filter to be applied. """ - filters = DashboardFilters(None, None, None, None, None) + filters = DashboardFilters(None, None, 0, datetime.min, datetime.max) handler = DashboardHandler() search_cache = dict() search_eviction = Queue() From 03769c366dc65680193c177d625c0bfdc915c484 Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Tue, 5 Dec 2023 14:50:02 -0500 Subject: [PATCH 06/47] removed none checks --- src/astra/usecase/alarm_handler.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/astra/usecase/alarm_handler.py b/src/astra/usecase/alarm_handler.py index b716c1c..8064c98 100644 --- a/src/astra/usecase/alarm_handler.py +++ b/src/astra/usecase/alarm_handler.py @@ -199,12 +199,10 @@ def _determine_toggled(cls, alarm: Alarm, filter_args: AlarmsFilters) -> bool: # First, checking if it satisfies priority requirements show: bool = True - if filter_args.priorities is not None: - show = alarm.priority.name in filter_args.priorities + show = alarm.priority.name in filter_args.priorities # Next, checking if it satisfies criticality arguments - if filter_args.criticalities is not None: - show = show and alarm.criticality.name in filter_args.criticalities + show = show and alarm.criticality.name in filter_args.criticalities # Checking if the alarm type matches if filter_args.types is not None: @@ -218,21 +216,18 @@ def _determine_toggled(cls, alarm: Alarm, filter_args: AlarmsFilters) -> bool: # Now we need to make sure the alarm fits in the time parameters alarm_confirm_time = alarm.event.confirm_time alarm_register_time = alarm.event.confirm_time - if filter_args.confirmed_start_time is not None: - compare_time = filter_args.confirmed_start_time - show = show and alarm_confirm_time >= compare_time - if filter_args.confirmed_end_time is not None: - compare_time = filter_args.confirmed_end_time - show = show and alarm_confirm_time <= compare_time + compare_time = filter_args.confirmed_start_time + show = show and alarm_confirm_time >= compare_time - if filter_args.registered_start_time is not None: - register_time = filter_args.registered_start_time - show = show and alarm_register_time >= register_time + compare_time = filter_args.confirmed_end_time + show = show and alarm_confirm_time <= compare_time - if filter_args.registered_end_time is not None: - register_time = filter_args.registered_end_time - show = show and alarm_register_time <= register_time + register_time = filter_args.registered_start_time + show = show and alarm_register_time >= register_time + + register_time = filter_args.registered_end_time + show = show and alarm_register_time <= register_time # Finally, checking if we only show unacknowledged alarms if filter_args.new: From 53f6dd533dc48da7d30a3d1a9e318125767f59ae Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Tue, 5 Dec 2023 15:22:59 -0500 Subject: [PATCH 07/47] completed documentation --- src/astra/usecase/alarms_request_receiver.py | 357 +++++++++---------- 1 file changed, 175 insertions(+), 182 deletions(-) diff --git a/src/astra/usecase/alarms_request_receiver.py b/src/astra/usecase/alarms_request_receiver.py index e179407..44f082d 100644 --- a/src/astra/usecase/alarms_request_receiver.py +++ b/src/astra/usecase/alarms_request_receiver.py @@ -30,17 +30,16 @@ class AlarmsRequestReceiver(RequestReceiver): """ - AlarmsRequestReceiver is a class that implements the RequestReceiver interface. - It handles requests from the alarms tab, such as creating the initial data table, - updating the currently represented information, changing the index of the datatable - that we are viewing, adding or removing priorities, criticalities or types - from the sets of them that we are viewing, and updating the sorting filter to be applied. + Handles requests relating to displaying alarms, such as creating the initial alarm table, + updating the currently represented information, adding or removing priorities, + criticalities or types from the sets of them that we are viewing, + and updating the sorting filter to be applied. """ filters = AlarmsFilters(None, None, CRITICALITIES, PRIORITIES, ALL_TYPES, datetime.min, datetime.max, datetime.min, datetime.max, False) handler = AlarmsHandler() - previous_data = None + previous_data = TableReturn([], []) _sorting = [-1, 1, 1, 1, 1, 1] _new = False _priorities = {'WARNING', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL'} @@ -50,14 +49,13 @@ class AlarmsRequestReceiver(RequestReceiver): @classmethod def create(cls, dm: DataManager) -> TableReturn: """ - Create is a method that creates the initial data table, - with all priorities/types/criticalities shown, no sorting applied and at the first index. + Creates a table of data using all alarms and applies any relevant filters or sorts - :param model: The model of currently shown data :param dm: Contains all data stored by the program to date. """ if cls.filters.tags is None: - # Since this needs to be supplied externally, we make a special case for it + # Since the filter tags list needs to be provided externally via the DataManager, a + # special case needs to be made to update the filter tags cls.filters.tags = set(dm.tags) # Create the initial table. @@ -67,40 +65,31 @@ def create(cls, dm: DataManager) -> TableReturn: @classmethod def update(cls): """ - update is a method that updates the currently represented information - - :param previous_data: The previous table that was in the view and we want to update. - :param dm: Contains all data stored by the program to date. + Updates the current data in the table to represent changes to filters or sorting """ - if cls.previous_data is not None: - cls.handler.update_data(cls.previous_data, cls.filters) + + cls.handler.update_data(cls.previous_data, cls.filters) @classmethod def set_shown_tags(cls, tags: set[Tag]): """ - Sets the filtered tags to be equivalent to - - :param tags: The new set of tags to exclusively include in the table + Setter method for """ cls.filters.tags = tags @classmethod def add_shown_priority(cls, add: str) -> bool: """ - add_shown_priority is a method that adds a priority to the set of priorities - that we are viewing. It returns True if it was successful and False otherwise. + Adds an alarm priority to the set of priorities that we are viewing. + It returns True if it was successful and False otherwise. :param add: the priority that we want to add to the set of priorities that we are viewing. - :returns: True if the priority was successfully added and False otherwise. + :returns: True iff the priority was successfully added """ - # Make sure that the set of priorities that we are viewing is not None. - if cls.filters.priorities is None: - cls.filters.priorities = set() - # Determine if we can add to the set of priorities that we are viewing. if add not in cls.filters.priorities: - cls.filters.priorities.add(AlarmCriticality(add)) + cls.filters.priorities.add(add) return True else: # was already in the set of priorities that we are viewing. @@ -109,21 +98,17 @@ def add_shown_priority(cls, add: str) -> bool: @classmethod def add_shown_criticality(cls, add: AlarmCriticality) -> bool: """ - add_shown_criticality is a method that adds a criticality to the set of criticalities - that we are viewing. It returns True if it was successful and False otherwise. + Adds a criticality to the set of criticalities that we are viewing. Returns True iff + it was successful :param add: the criticality that we want to add to the set of criticalities that we are viewing. - :returns: True if the criticality was successfully added and False otherwise. + :returns: True iff the criticality was successfully added """ - # Make sure that the set of criticalities that we are viewing is not None. - if cls.filters.criticalities is None: - cls.filters.criticalities = set() - # Determine if we can add to the set of criticalities that we are viewing. if add not in cls.filters.criticalities: - cls.filters.criticalities.add(add) + cls.filters.criticalities.add(add.name) return True else: # was already in the set of criticalities that we are viewing. @@ -152,35 +137,139 @@ def add_shown_type(cls, add: str) -> bool: return False @classmethod - def set_shown_priorities(cls, priorities: set[AlarmPriority]): + def toggle_sort(cls, heading: str) -> bool: """ - Sets to the set of priorities to be shown + Method for toggling sorting on a specific heading + The headings include: + - TAG + - PRIORITY + - CRITICALITY + - REGISTERED + - CONFIRMED + - TYPE + This method will filter the data according to which heading was toggled + + :param heading: the sort name that was toggled + """ + sort_value = 1 + for i in range(len(ALARM_HEADINGS)): + check_heading = ALARM_HEADINGS[i] + if check_heading == heading: + cls._sorting[i] *= -1 + sort_value = cls._sorting[i] * (heading == check_heading) + else: + cls._sorting[i] = 1 - PRECONDITION: is an element of + if sort_value == 1: + # ascending + cls.update_sort(('<', heading)) + elif sort_value == -1: + # descending + cls.update_sort(('>', heading)) - :param priorities: a set of priorities to show + return sort_value == 1 + + @classmethod + def toggle_priority(cls, tag: Tag): """ - cls.filters.priorities = priorities + Method for toggling filtering of specific priority + The headings include: + - WARNING + - LOW + - MEDIUM + - HIGH + - CRITICAL + This method will filter the data according to which heading was toggled + + :param tag: The priority name to toggle on/off + """ + if tag not in cls._priorities: + cls._priorities.add(tag) + else: + cls._priorities.remove(tag) + + cls.set_shown_priorities(cls._priorities) + cls.update() + + @classmethod + def toggle_criticality(cls, tag: Tag): + """ + Method for toggling filtering of specific criticality + The headings include: + - WARNING + - LOW + - MEDIUM + - HIGH + - CRITICAL + This method will filter the data according to which heading was toggled + + :param tag: string representing which criticality was toggled + """ + if tag not in cls._criticalities: + cls._criticalities.add(tag) + else: + cls._criticalities.remove(tag) + + cls.set_shown_criticalities(cls._criticalities) + cls.update() + + @classmethod + def toggle_type(cls, tag: Tag): + """ + Method for toggling filtering of specific criticality + The headings include: + - RATE-OF-CHANGE + - STATIC + - THRESHOLD + - SETPOINT + - SOE + - LOGICAL + This method will filter the data according to which heading was toggled + + :param tag: string representing which type of alarm was toggled + """ + + if tag not in cls._types: + cls._types.add(tag) + else: + cls._types.remove(tag) + + cls.set_shown_types(cls._types) + cls.update() @classmethod - def set_shown_criticalities(cls, criticalities: set[AlarmCriticality]): + def toggle_all(cls) -> None: + """ + Resets all non-parameter filters to their default options. That is, + all alarms of different acknowledgement levels, priorities, criticalities, and types are + shown """ - Sets to the set of criticalities to be shown - PRECONDITION: is an element of + cls.set_new_alarms(False) + cls.set_shown_priorities(cls._priorities) + cls.set_shown_criticalities(cls._criticalities) + cls.set_shown_types(cls._types) - :param criticalities: a set of criticalities to show + @classmethod + def set_shown_priorities(cls, priorities: set[str]): + """ + Setter method for """ - cls.filters.criticalities = criticalities + + cls.filters.priorities = priorities @classmethod - def set_shown_types(cls, types: set[str]): + def set_shown_criticalities(cls, criticalities: set[str]): + """ + Setter method for """ - Sets to the set of types to be shown - PRECONDITION: is an element of + cls.filters.criticalities = criticalities - :param types: a set of types to show + @classmethod + def set_shown_types(cls, types: set[str]): + """ + Setter method for """ cls.filters.types = types @@ -201,7 +290,7 @@ def remove_shown_priority(cls, remove: AlarmPriority) -> bool: # Determine if we can remove from the set of priorities that we are viewing. if remove in cls.filters.priorities: - cls.filters.priorities.remove(remove) + cls.filters.priorities.remove(remove.name) return True else: # was not in the set of priorities that we are viewing. @@ -224,7 +313,7 @@ def remove_shown_criticality(cls, remove: AlarmCriticality) -> bool: # Determine if we can remove from the set of criticalities that we are viewing. if remove in cls.filters.criticalities: - cls.filters.criticalities.remove(remove) + cls.filters.criticalities.remove(remove.name) return True else: # was not in the set of criticalities that we are viewing. @@ -278,48 +367,41 @@ def update_sort(cls, sort: tuple[str, str]) -> bool: @classmethod def set_registered_start_time(cls, start_time: datetime): """ - Modifies to be equal to - - :param start_time: the datetime to be set + setter method for """ + cls.filters.registered_start_time = start_time @classmethod def set_registered_end_time(cls, end_time: datetime): """ - Modifies to be equal to - - :param end_time: the datetime to be set + setter method for """ + cls.filters.registered_end_time = end_time @classmethod def set_confirmed_start_time(cls, start_time: datetime): """ - Modifies to be equal to - - :param start_time: the datetime to be set + setter method for """ + cls.filters.confirmed_start_time = start_time @classmethod def set_confirmed_end_time(cls, end_time: datetime): """ - Modifies to be equal to - - :param end_time: the datetime to be set + setter method for """ + cls.filters.confirmed_end_time = end_time @classmethod def set_new_alarms(cls, new: bool): """ - Sets the to . - - :param new: the value to set to. True indicates - that we will only show new/unacknowledged alarms, False indicates that we will - show all alarms. + setter method for """ + cls.filters.new = new @classmethod @@ -327,6 +409,7 @@ def toggle_new_only(cls) -> None: """ Switches the boolean value of """ + cls.filters.new = not cls.filters.new @classmethod @@ -337,6 +420,7 @@ def acknowledge_alarm(cls, alarm: Alarm, dm: DataManager) -> None: :param alarm: The alarm whose acknowledgment needs to be modified :param dm: Stores all data known to the program """ + cls.handler.acknowledge_alarm(alarm, dm) @classmethod @@ -347,11 +431,15 @@ def remove_alarm(cls, alarm: Alarm, dm: DataManager) -> None: :param alarm: The alarm to remove :param dm: The holder of the global alarm container """ + cls.handler.remove_alarm(alarm, dm) @classmethod def get_alarm_banner(cls) -> list[str]: - """Returns a list of strings in order to show in the alarm banners""" + """ + Returns a list of strings in order to show in the alarm banners + """ + return cls.handler.get_banner_elems() @classmethod @@ -359,140 +447,45 @@ def install_alarm_watcher(cls, dm: DataManager, watcher: Callable) -> None: """ An interfacing function for the alarm container to install a watcher function """ + dm.alarms.observer.add_watcher(watcher) @classmethod def get_table_entries(cls) -> list[list]: - if cls.previous_data is not None: - return cls.previous_data.table - return [] - - @classmethod - def get_new(cls) -> bool: - return cls.filters.new - - @classmethod - def get_priorities(cls): - return cls._priorities - - @classmethod - def get_criticalities(cls): - return cls._criticalities + """ + getter method for the data to actually be shown in the alarms table + """ - @classmethod - def get_types(cls): - return cls._types + return cls.previous_data.table @classmethod - def toggle_sort(cls, heading: str) -> bool: + def get_new(cls) -> bool: """ - Method for toggling sorting on a specific heading - The headings include (for now): - - TAG - - PRIORITY - - CRITICALITY - - REGISTERED - - CONFIRMED - - TYPE - This method will ask the model to sort the data - according to which heading was toggled - - Args: - heading (str): string representing which heading was toggled + getter method for whether only unacknowledged alarms should be shown or not """ - sort_value = 1 - for i in range(len(ALARM_HEADINGS)): - check_heading = ALARM_HEADINGS[i] - if check_heading == heading: - cls._sorting[i] *= -1 - sort_value = cls._sorting[i] * (heading == check_heading) - else: - cls._sorting[i] = 1 - - if sort_value == 1: - # ascending - cls.update_sort(('<', heading)) - elif sort_value == -1: - # descending - cls.update_sort(('>', heading)) - return sort_value == 1 + return cls.filters.new @classmethod - def toggle_priority(cls, tag: Tag): + def get_priorities(cls): """ - Method for toggling filtering of specific priority - The headings include (for now): - - WARNING - - LOW - - MEDIUM - - HIGH - - CRITICAL - This method will ask the model to sort the data - according to which heading was toggled - - Args: - heading (str): string representing which heading was toggled + getter method for the priorities currently shown """ - if tag not in cls._priorities: - cls._priorities.add(tag) - else: - cls._priorities.remove(tag) - cls.set_shown_priorities(cls._priorities) - cls.update() + return cls._priorities - def toggle_criticality(cls, tag: Tag): + @classmethod + def get_criticalities(cls): """ - Method for toggling filtering of specific criticality - The headings include (for now): - - WARNING - - LOW - - MEDIUM - - HIGH - - CRITICAL - This method will ask the model to sort the data - according to which heading was toggled - - Args: - heading (str): string representing which heading was toggled + getter method for the criticalities currently shown """ - if tag not in cls._criticalities: - cls._criticalities.add(tag) - else: - cls._criticalities.remove(tag) - cls.set_shown_criticalities(cls._criticalities) - cls.update() + return cls._criticalities @classmethod - def toggle_type(cls, tag: Tag): + def get_types(cls): """ - Method for toggling filtering of specific criticality - The headings include (for now): - - RATE-OF-CHANGE - - STATIC - - THRESHOLD - - SETPOINT - - SOE - - LOGICAL - This method will ask the model to sort the data - according to which heading was toggled - - Args: - heading (str): string representing which heading was toggled + getter method for the types of alarms shown """ - if tag not in cls._types: - cls._types.add(tag) - else: - cls._types.remove(tag) - - cls.set_shown_types(cls._types) - cls.update() - @classmethod - def toggle_all(cls) -> None: - cls.set_new_alarms(False) - cls.set_shown_priorities(cls._priorities) - cls.set_shown_criticalities(cls._criticalities) - cls.set_shown_types(cls._types) + return cls._types From 0b59c6479b3960cbfa9c3ecf19372ed5c4e3d0a1 Mon Sep 17 00:00:00 2001 From: shape-warrior-t Date: Tue, 5 Dec 2023 15:49:20 -0500 Subject: [PATCH 08/47] Change window icons --- requirements.txt | 4 ++-- src/astra/frontend/startup_screen.py | 6 ++++-- src/astra/frontend/view.py | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 98d1eaf..ba3183e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ PyYAML~=6.0.1 SQLAlchemy~=2.0.23 h5py~=3.10.0 pandas~=2.1.3 -pytest -matplotlib~=3.8.2 \ No newline at end of file +pytest~=7.4.3 +matplotlib~=3.8.2 diff --git a/src/astra/frontend/startup_screen.py b/src/astra/frontend/startup_screen.py index 25e9bcf..049ce89 100644 --- a/src/astra/frontend/startup_screen.py +++ b/src/astra/frontend/startup_screen.py @@ -1,8 +1,9 @@ """This module provides the GUI for Astra's startup screen.""" -from tkinter import Button, Frame, Label, Tk, Toplevel -from tkinter import filedialog, font, messagebox, simpledialog, ttk, Event from tkinter import BOTTOM, LEFT, X, Y +from tkinter import Button, Frame, Label, Tk, Toplevel +from tkinter import filedialog, font, messagebox, simpledialog, ttk, Event, PhotoImage + from astra.frontend.view import View from astra.usecase.request_receiver import DataRequestReceiver @@ -14,6 +15,7 @@ def __init__(self): """Initialize the screen, adding all the UI elements.""" super().__init__() self.title('Astra') + self.iconphoto(True, PhotoImage(file='logo.png')) self.state('zoomed') self.controller = DataRequestReceiver() diff --git a/src/astra/frontend/view.py b/src/astra/frontend/view.py index 0f97de9..9f531a2 100644 --- a/src/astra/frontend/view.py +++ b/src/astra/frontend/view.py @@ -3,9 +3,9 @@ """ import itertools -from tkinter import Frame from tkinter import BOTH -from tkinter import ttk, Tk, Label +from tkinter import Frame, Label, Tk +from tkinter import ttk, PhotoImage from astra.data.data_manager import DataManager from .graphing_view import GraphingView @@ -33,6 +33,7 @@ def __init__(self, device_name: str) -> None: # Root frame of tkinter super().__init__() self.title(f'Astra - {device_name}') + self.iconphoto(True, PhotoImage(file='logo.png')) # tab widget tab_control = ttk.Notebook(self) From 8e7c0f4e60bf98b65ea52bf5e7afd336d9a15b9e Mon Sep 17 00:00:00 2001 From: shape-warrior-t Date: Tue, 5 Dec 2023 15:53:11 -0500 Subject: [PATCH 09/47] Add logo image file --- src/logo.png | Bin 0 -> 12948 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/logo.png diff --git a/src/logo.png b/src/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a55c531cc1baf9906e2f45548317f412753ec2c4 GIT binary patch literal 12948 zcmeIZRa}(O_cuD=NDI=^&%+1Onl9|9epS?2Al6AZ}(^NpUqd zgM&r%BzjHvzsEv_CC!DNU1+l41MAbCcnrolNlcoYPdS_z@S)n_KL!oIX3?I?OUSTc zfN^JDEZb1n!k&-4U<21-kYtHfq2x~e9kNx?q0zFc70xbvubw_%eT!LxW$7xErt0$0 z9)#{?zqg8_!E=AHBjh4SW&*7JYyO`~5Xdc<77TI=|Nr|-fy~4djMR2*6ULB8p8x14 zoz6t(tC)-N?MtulNSO0IvIu6BGEA?aE>49pwMr`7@k0NDCYlau7fG#Fc+h@jWLcWq z=qg#qxDvc$tnswf@uT@xiL%$8(J|9Wj;HysiSzRBhtD<)140CXEJQR%D{JW%(2^8- z@oHt(0CY8*A2UC%4tJN%lVZJp=;5Tuj==}919}-YKJ0z)-T=2^h+u>~7u*;b+$@n5 zX*Y2<)-ga7dx&sVQa69S#=Plq`oqv+PegN9>ob4NDz6%DPM+dfxwUBVCf*`6kz2R@ zw!|)h6x&M&z4O=T;4<6>(PFIkQx*iBstiG8KFX7~I=zIN1)`aCy}Zk=U5-$_SM^x8KR4A(Q8e zO&f3jXwuII-5`Q~Z?RI$w2Enzs4`?G@uB)`nLI0V)7`ZKTD(O$kq!c4m=ca#|s&Cl}JUgpo2)sapEvClH?#)?J%8vRV>qvZ~# zm1G=-86v3-DXc3V8`q3PR1EkvM0UO+#J(uO`(wOf6DucYcAS^j-f-S+2>m&7>2WSn z+Sg2NV8z##z^A3yWLid2-)W(y)kjg1s%2;yJmMmJyCD*S<#k&v66?QSy?GwUEZ;Ho zTHKIFPo46i!L&n`h=$U|;Up4${4l7AnM|9Xx{`&>Q|~wro8bvZHpVJt^*RH~94REh z*FCl*Tj#1_px{ytZ9xpbi>CH~^A8bnL4Lfe#d{n%n< z7Br$P&a-2`?Q(CDW`=bOk(lLe-g~QjF#pG)-SmnnTyZk>K^E2M2^2$Er?eycEcI!v zAp-j-nST}2`U*S7cPR&B%*M%ciBzM~nD{&TM;8oMh{Tzi({8UGb`R$mj1}AC6gg`s zht?EHD;e1{!5KQ9DFgB@?ja(@8htTa1La045YaR#{>8>|6C;RFAje4_AnfKljKOh6 zhkoY}67*UXyZe-Z|E;d*BiD^W@;8nwj4|Tla?+yR%lCYPQ(`rouk(3@in;DyBgCYY z;oB{{(~0V|9OyZxyZMEQ)9oKnku*d!LdgfTmnmS)B;%JccRmR~sqe8z& z6fd2{$X%&oifIySpi{hKgs|pAuU?&1f(uHij^e;n%{$&Y z?FK)|rH%qo0vs#NI(Pm%%h9vQItGQQc4;hTdfeg88qv|6jp3-~_67Wtfv-1UjptUN zO_loy36T&9JG;PMPd+3$PcwZb3^R`Z-H%Hyf)nz*9pv5&2S{kS!E!tUfzD#gryPm5qPrWE`?2g&tpN(-GMIPI(&E;C$ zTdyzDVy;tCV1u2XEw}vffGQygqIc?zDbL+}d+#5hs~g}Z_)Au&{FhLpQ&xw-RNt#& z5mvp^mzW*V!1BOff|h2y5w`!o4q~Op-e<=r7j=GUnx*zNBps0 z5#5}u#0`6B1h0rfBo>xO>-4=_9)504)_CSKrpfbb+$2JyBNP~PPVKHKD&*|EY+mqg z**H!v=wspamoC|~PVfy6tX-+ceDhR>3yxPTW2Mv&?w9t&NtBbW{rNNerm@a>X1bor z@J>9XZo2kac}!Mq!}-Of0kPkhJpzYP0)LHkZ6{H>xZNUXq%SgprL?PxpxmpnB~E&C zdO0(*Qr%CsT;v{&@0pxOShegiHW7{Fr}@E~8pB&o6~ek{^m9|ForZUxwPU{4)hKUx zt+huj45p$!5qUs;+a9wj5b`|eFOi6w*K56~{cnO=6>=2a=ROQ`o}1I}ibh6>hBG9z zE(>uLYUCUh1t1bV^=TCBZjOkanrR;)>urVnRXl{e8yB`fYZ=i}Icy?G*y5wtN8XbU z1S_l|NE__t5oUpQcA9pbz4ojc8l6XRgdo;wGbb}ctn%mTjk}U$>GSZ%YA~Q}&t?`T zsqvEb7&aobTMjr=@c5sFJ+`*M8QwIk$7FA$TKyh6J_w$=x*9-e%K7?0Bt9&*o-6c0 z-$g|UpBpJubG@!Q{)pIyyIoRr-6+U0#GG0j)fmO^2?Xa*KqRQCT8`@zi3=DupnF@h zN#nLcEw-o22>I=Z%?nb=&akFs%%VmkG)47N&RCGW68s@qh#kZF%{*CB(I3a-*ZJq} zZI7dTI~6=GJVBbz*H5Z*YSZL)Nkfo4*FxgFvUKIxh>q&qDbfM??ehEir8+$?!{VDU zQ4KomwG*CGuOigHL9}PGHtd$4PrhDHZm0ZKBfFJGRFyQHrb(Rs@)M0pwI^RYDIU{l zqdR{J-NxoQhsB518kM4MO}O}{zM~-+i!_-BPTC~;Y%8MY%kjVJqj}g0Nm~}ZSI=>+>+TieR zluI@qq$_Q(QwO;nZw< z>--HJ<>$HYdiAzQ=TU?w)GO~=3V%~G;qp6w%tOOf(!f!stklI+{bPMhlY#`=k4WA= zV2*)(9VA!?IXKy$$UtYfc3>fCttc@~A8!$0(=a0%RC7oChTC&uPCN=hQdUqpIqOnm z6aif~EZ-R}4yLz|)4DgWxJfo?4m~eORLjXU zKPJKEIkK(2(I<$)CNi!Vg59rXT`OMTHe+dmyUS2pRp-z9nl22J=T*rIy~cXl<;6IZ z;MdBF*L8AV%i%-F(}1lr`xZ%pyI93`B?k2BbGA_HuX=wrIAz5yl;Lmimp^;gIE2H) zic0zN$3DpW_Tm0*`Ko=#DIS&2L^VB?E1#R0=o^-caoVxq{VnA-5YDy|^6I6f_g?62 zZl-L%AFp11&^sm+ZGd9pEeAC+7Mls;xtdj+M0hN7r#Dq?>JlVLmbJgnv@!eHLby&# z1X^ndmY6#~F)CFfu8^B}@@Ow%BG)aDr05kv;-O+Dys$g!;C{xxbVGHUXM@$_e&y74 z0Vji&;v!|1`^zK(1QvdnH!1Jp$dY?QxCX!6Yam=JLL%6C1{MF~SnEm7$@MO~hMVtFISqF7Ww$NV>>Zr&-x2;2| zV}8sE>(je~BYikw9jd+IW3#zRFmx=hMDMKhCyR zIu@3ke3OejNdHr;>{6XVB56_V;#1gHX`kmGqaQz{$4U6ppLQtbY8pFm=-I&P`&xyp zzY&=Rr&QQm-UA1R24k~#z=ZHjkF)1Z@GWXV5&~WeVA8gJyt8s%2PxP~-+pW(& zV=A&db?p&0q(jjb!#9`OGbxzgDk?UAln><3pu^lI=R1iP!;^K0v0h*#{OLQ)J%_yU zCF^ZwXpliSnL%KjNqXJd6Z^G}*k#K4$GjYzePY}K(40bTT_ag9&r-F`4eF1=` z(ue7qov>5-X}_vHd?v#>PWLved%-5@;w-hT`7xXF?NelEmX_ELtC2d;^kVV!`_K@0F3pKVA zjkhTrf73BtQfz%9zzspP&T82g`pFAjFO>xk@C)@eH$_#-H%o;d#84^U<2OOly|Xo3 zFmYGC#Z5ct6x;SIHYXSa-#-;+xXu6z2GTk!9we{}wMzTF*J1D1c|PaR#e9e@K{`rT z?)b7k79z1#q`hS)!Yz)ZbPEQ*A`EQ*d8GvFJ1cIT(ogH#qYOc^BPy!p##+Dvl->oB z#(eFq{VCC<;RcM=bYU!fZJa!5;`mL_Df?)|vtMA>-q9Do-6~99sZRE{|N5po%88@S z4lEGd@Qv0PF=8aBX5Q=dApt?gH=^%^3Ed8H=qxXva;#!|(WrwOWfmWvnBR+uX}7KFn)KE}*7p-p zHsNN$G674_ML)gOYw}x7OtFidXViA^TmnjEr)!#A?`({8n|SG1<2Tg6m@{ z>KzA0hfI|zwu+;c; zl?(-WQV$BKD?xwgNcnSLW7b*mEVo>L#oT@qf{bv!)Q;b_!>rr+$!d=-#!1Y7|K3*b zO^k4vmo(!UCDby=y5S@)xJhSJ-~T9qOr-k-Q(N*FiY8WhHU_(lop&x4rTYM!CES39 zZY8s8*NLQyNv0~CHK0xjP6Dr`9x&Byz1!nPX)6i)`s&#YgI29u{t*dD0ErU3^~+2i zPbyFMpZwapX_Rj-Z&P4(0oVfl@RvClo}ZE(*EBhv$#ouS(u0<4q!dL`CD3byG_gD* zX!ncf77v5{-e77ZkAH`ONL-`5d!niGo>mZDP(o)_$GR@#e)?=2`e{>FJnCYCaD!P3 z8>{@)Qm+B|#gN4W>B?XUdKZnz(7;RVn`6dWFkJrq&dN*5dG)oz`J8Mrl90&wFmHl0 z4pDK0&kYh(F))rm3s;M1VVcm{^f3Ui3KR#^p39+5JD+_FTOz6>9wbr}GQHuXL^ z=hh~^d<$g+;W6qx3UF(yum%VJiBK}+!BOA|Hsvu*+qC7;P{fo{HE<4(Pd*o+c< zF@9f2o>5nAWn_KY*Y=0cMEO5$7e%K39{)?C)e=K2gRD+j*jF)BUz0c@HbdsHF2>Q^ zZT^s|W(gj;ScY&1Ek;lu$AaafoN@NCA$PVgq>CdZ-J^P(pA(P6fQs@u3Jj9}crTY7 z%ApAs3@OMd%vp9285SWL0` z@gs0D+ncKUVeae+NLPi7U9(K7mftfgE_y;=MGWZ&D5E^uR2*!ZH>8R+#(fY^Uyh}n zh0qr_Ig*+^_8CihJZ8ES{+EEoAZteJs;W%_Opp*g`HNTt8db{f$h$b-h#U;z%BI1S zi3szUJ_I<+Np5fHlTPO6Pp`ij%(CzZh0yM|e73%R@tv4Zq#I{DM91hFTrQP&YM0m%8=BKzF?RgPrj z8zSSNDbfpOPoE+w#~CnE-|-s3J!kasBI3(S=&bRdt00n5QiYr;Zj|kawcoD%6Cv;n zY!{x^ds@ux=OIT5+I z0v$HwfbC}*xHuBv$ZR(X+V#T~voM4U`I+C3YI}WztT0pQGzaHoZ4EDlGGqZtzVB?_ z1I~L3MWFz0rPSgbVlQ?5ABSuzF}v4RYq)z8VGGmXcNW4)YFl#nVO0#DcH)2gZS z4JesAQ;ED2Kj9fR`&F)S>TSTfjV=r*S!`UNC-jK|tdfD9E1K+4@nTkZfW;8fh07Iq zpzB+>yQ$25u9tRHYij}Y;1brwj!Vz%e_E}`2;z^EE`M8XknhBu{g%iW{VeqnhwUJT z0~coy+`*G=hvs@@+l32I;MF)^vWMc15+f)<>+WO0*vkAEP|U-IiFU>kp^eH`ebF1n zU3;C02;*0RAEcy<8Wv<=cjv%B`^p336vfK#SHrn;pAi{1qfJGc?KTFSMRTK6NmqJH zPiT1(JpC<=Z=Te9U=8}q8iK`5;}7Zj!q*s&eq=!RqmFi=e(LvB@IdB-Sw9Wu7Q=Jo};x6zaV3E{1{9 z@B{dllu09(GGfT&CGa^G_v@CUF+ux3YPD$uAYJ(sY=#Yz%$PuBM?13oiM4`B3L2=M>>a#KH z`T>-WOk3gVIF{k~(h>RHbFa=S!{s%Vc(_qIXdFKt^eD5DK^xROmn&>VTv2@o%_kgL zKjeu60yONk)Ku(GFeu_Rz@t%AM8>~(Cq7#ZUlV`?3l>PV-FYc~i{qPze;kCYRD2G$ zf&a4s!f?$%zM1lk9CK(PTqd-`@3=vi7F%_jEi3xOpwGSxtK-UW&_B#2Z@Q;N26E%F zRo52^aR@fwBEMTfae!l`{C9msOw5J@PaGu0M^lA45`0Z0@j1x$#6h^qBGsP75DAd) z%>Vl8f69NvP7G}@wMC+?giIE#^vE-;ya1+B`z|5si{c`esIUzb91);m@P>XT6~`FyDwIXuB{ht8dy-s#PK=eTRCZoZu3wycg}oa z{kS0x#fmMwvey$&9N>S(!oRJ0=>Iy>XmgfDauR}Oq1ScaX!V5h_tUIQudInWqAvt~ z{r3Plus(f7Q#Ch__+$l&sQSG2KZjUYw$Nyz_Ii$n2lP)mUBsOm{JRmk07vo+jVwoD zCIif+Kb-c85X5HCxrNaK%fU#ju-~BLM)7^SPiIdEO_FuB^~wY|(s*l?Yk=}QH7gI` ze;mq_PHbZ)1X&b08x$CDm(12nn6qI5jVT4N1h*nWB^Kiv>w$a02o%ne=(@z^5z(x? z@NAC@c{2U`J${|paj82+q@L{B(_~PC@t0#1824q&>yqdM1|wNWS6$oyYk3;4A&!km z{Bws8yZxG<5Zi)3YqDGN9WhGqPsL~2x7JHZl8hkSZ9MzL;`*+l4~%TYz)U84rS4|t zTf5v(jd^+(E$vv5|!^mZxz|sjwWqTtc~{`69-X zQsu-N$sI}!MbR?blMw zkyn*B409*|8_gIw2f^Nw2DYF$qXDW#mJ6afs(mMh^(uS|=x~3ooDBG)t9WuCam8CV zbR2}JU-Ig8Jz2a42ZntZ34ufV64E-av#w1B=pvU33%$(#f4XS$s2z~J*@`Lk2J|Rm zc<0Hf`?hhaowld+$$$f!3ooU5-V7D_KDy4iDHb!tKh;zU(1sDg;ptniYtneY9N0EEq{>8x8mU7)L4TI^!oaMvaRLF4_Bh?XurA(84r_6-%|i2 z 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 literal 0 HcmV?d00001 From ea89d3621db68403a22a8eb659c653120d5d0b8d Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Tue, 5 Dec 2023 16:09:24 -0500 Subject: [PATCH 10/47] documented dashboard handler and added more filter default values --- src/astra/usecase/alarm_handler.py | 5 ++- src/astra/usecase/alarms_request_receiver.py | 4 +-- src/astra/usecase/dashboard_handler.py | 32 ++++++++++++-------- src/astra/usecase/filters.py | 4 +-- src/astra/usecase/request_receiver.py | 3 +- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/astra/usecase/alarm_handler.py b/src/astra/usecase/alarm_handler.py index 8064c98..3d83f4e 100644 --- a/src/astra/usecase/alarm_handler.py +++ b/src/astra/usecase/alarm_handler.py @@ -209,9 +209,8 @@ def _determine_toggled(cls, alarm: Alarm, filter_args: AlarmsFilters) -> bool: show = show and cls._get_alarm_type(alarm) in filter_args.types # Checking if the tag of the alarm is requested to be shown - if filter_args.tags is not None: - relevant_tags = set(alarm.event.base.tags) - show = show and len(relevant_tags.difference(filter_args.tags)) == 0 + relevant_tags = set(alarm.event.base.tags) + show = show and len(relevant_tags.difference(filter_args.tags)) == 0 # Now we need to make sure the alarm fits in the time parameters alarm_confirm_time = alarm.event.confirm_time diff --git a/src/astra/usecase/alarms_request_receiver.py b/src/astra/usecase/alarms_request_receiver.py index 44f082d..526738e 100644 --- a/src/astra/usecase/alarms_request_receiver.py +++ b/src/astra/usecase/alarms_request_receiver.py @@ -36,7 +36,7 @@ class AlarmsRequestReceiver(RequestReceiver): and updating the sorting filter to be applied. """ - filters = AlarmsFilters(None, None, CRITICALITIES, PRIORITIES, ALL_TYPES, datetime.min, + filters = AlarmsFilters({Tag("")}, None, CRITICALITIES, PRIORITIES, ALL_TYPES, datetime.min, datetime.max, datetime.min, datetime.max, False) handler = AlarmsHandler() previous_data = TableReturn([], []) @@ -53,7 +53,7 @@ def create(cls, dm: DataManager) -> TableReturn: :param dm: Contains all data stored by the program to date. """ - if cls.filters.tags is None: + if "" in cls.filters.tags: # Since the filter tags list needs to be provided externally via the DataManager, a # special case needs to be made to update the filter tags cls.filters.tags = set(dm.tags) diff --git a/src/astra/usecase/dashboard_handler.py b/src/astra/usecase/dashboard_handler.py index ba23fe3..ff62116 100644 --- a/src/astra/usecase/dashboard_handler.py +++ b/src/astra/usecase/dashboard_handler.py @@ -1,11 +1,11 @@ -import queue +from queue import Queue from typing import Iterable from astra.data.alarms import ( Alarm, AlarmCriticality, - AlarmPriority, ) + from .filters import DashboardFilters from .table_return import TableReturn, TelemetryTableReturn from astra.data.data_manager import DataManager @@ -22,7 +22,7 @@ CONFIG = 'CONFIG' ROUNDING_DECMIALS = 2 # For now, this choice is somewhat arbitrary -CACHE_SIZE = 20 +CACHE_SIZE = 200 class DashboardHandler: @@ -31,9 +31,10 @@ class DashboardHandler: """ @staticmethod - def search_tags(search: str, cache: dict[str: Iterable[Tag]], eviction: queue) -> list[Tag]: + def search_tags(search: str, cache: dict[str, Iterable[Tag]], eviction: Queue) \ + -> Iterable[Tag]: """ - Searches for all tags where is a substring of the tag + Finds any tag where their tag name or description matches and returns them :param search: The substring to search for :param cache: Stores CACHE_SIZE + 1 keys, where each key matches a previous @@ -51,8 +52,12 @@ def search_tags(search: str, cache: dict[str: Iterable[Tag]], eviction: queue) - closest = prev_search max_len = len(prev_search) + # If search request is already in the cache if closest == search: return cache[search] + + # Search request is not already in the cache, so we perform the search on the closest + # option matching = [] for tag in cache[closest]: lower_tag = tag.lower() @@ -63,6 +68,7 @@ def search_tags(search: str, cache: dict[str: Iterable[Tag]], eviction: queue) - cache[search] = matching eviction.put(search) + # evicting from the cache if needed if len(cache) == CACHE_SIZE + 2: remove = eviction.get() cache.pop(remove) @@ -71,7 +77,7 @@ def search_tags(search: str, cache: dict[str: Iterable[Tag]], eviction: queue) - @classmethod def _format_param_value(cls, tag_data: ParameterValue | None) -> str: """ - Formats the . + Formats the for viewing in the telemetry dashboard :param tag_data: The (converted) data to format :return: A string with the appropriate formatting @@ -97,7 +103,7 @@ def _format_alarm_data(cls, alarm: Alarm | None) -> str: @classmethod def _tag_to_alarms(cls, tags: list[Tag], - alarms: dict[AlarmPriority, list[Alarm]]) -> dict[Tag, Alarm]: + alarms: dict[str, list[Alarm]]) -> dict[Tag, Alarm]: """ Finds and returns the highest priority alarm for each Tag in ", "TAG"), + 0, datetime.min, datetime.max) handler = DashboardHandler() search_cache = dict() search_eviction = Queue() From c39d64f85853cb9e723f0e5e93f1b4d3bff6f2e6 Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Tue, 5 Dec 2023 16:26:14 -0500 Subject: [PATCH 11/47] added docstrings for alarmsrequestreceiver attributes --- src/astra/usecase/alarms_request_receiver.py | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/astra/usecase/alarms_request_receiver.py b/src/astra/usecase/alarms_request_receiver.py index 526738e..b689c48 100644 --- a/src/astra/usecase/alarms_request_receiver.py +++ b/src/astra/usecase/alarms_request_receiver.py @@ -34,6 +34,30 @@ class AlarmsRequestReceiver(RequestReceiver): updating the currently represented information, adding or removing priorities, criticalities or types from the sets of them that we are viewing, and updating the sorting filter to be applied. + + :param filters: Container class for all filters to apply to the table + :type: AlarmsFilters + + :param handler: THe class that actually processes requests made by this request receiver + :type: AlarmsHandler + + :param previous_data: Stores the return value of the previous call to + :type: TableReturn + + :param _sorting: Tracks which direction each row should be sorted by + :type: list[int] + + :param _new: Tracks if only new alarms should be shown + :type: bool + + :param _priorities: Stores the priorities currently being shown + :type: set[str] + + :param _criticalities: Stores the criticalities currently being shown + :type: set[str] + + :param _types: Stores the types currently being shown + :type: set[str] """ filters = AlarmsFilters({Tag("")}, None, CRITICALITIES, PRIORITIES, ALL_TYPES, datetime.min, From 2ef291cf08fc14b04f94d10826665275cebc270a Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Tue, 5 Dec 2023 16:52:04 -0500 Subject: [PATCH 12/47] mypy fixes --- src/astra/frontend/tag_searcher.py | 2 +- src/astra/frontend/telemetry_view.py | 3 +- src/astra/usecase/dashboard_handler.py | 4 +- .../usecase/dashboard_request_receiver.py | 272 ++++++++++++++++ src/astra/usecase/request_receiver.py | 306 ++---------------- 5 files changed, 309 insertions(+), 278 deletions(-) create mode 100644 src/astra/usecase/dashboard_request_receiver.py diff --git a/src/astra/frontend/tag_searcher.py b/src/astra/frontend/tag_searcher.py index bd4c737..e7896f4 100644 --- a/src/astra/frontend/tag_searcher.py +++ b/src/astra/frontend/tag_searcher.py @@ -3,7 +3,7 @@ from astra.data.data_manager import DataManager from astra.data.parameters import Tag -from astra.usecase.request_receiver import DashboardRequestReceiver +from astra.usecase.dashboard_request_receiver import DashboardRequestReceiver class TagSearcher: diff --git a/src/astra/frontend/telemetry_view.py b/src/astra/frontend/telemetry_view.py index e314aa7..a3ea144 100644 --- a/src/astra/frontend/telemetry_view.py +++ b/src/astra/frontend/telemetry_view.py @@ -11,7 +11,8 @@ from astra.data.data_manager import DataManager from astra.frontend.timerange_input import OperationControl, TimerangeInput from .tag_searcher import TagSearcher -from ..usecase.request_receiver import DashboardRequestReceiver, DataRequestReceiver +from ..usecase.dashboard_request_receiver import DashboardRequestReceiver +from ..usecase.request_receiver import DataRequestReceiver class TelemetryView: diff --git a/src/astra/usecase/dashboard_handler.py b/src/astra/usecase/dashboard_handler.py index ff62116..d6f6c55 100644 --- a/src/astra/usecase/dashboard_handler.py +++ b/src/astra/usecase/dashboard_handler.py @@ -31,8 +31,8 @@ class DashboardHandler: """ @staticmethod - def search_tags(search: str, cache: dict[str, Iterable[Tag]], eviction: Queue) \ - -> Iterable[Tag]: + def search_tags(search: str, cache: dict[str, Iterable[str]], eviction: Queue) \ + -> Iterable[str]: """ Finds any tag where their tag name or description matches and returns them diff --git a/src/astra/usecase/dashboard_request_receiver.py b/src/astra/usecase/dashboard_request_receiver.py new file mode 100644 index 0000000..55571c3 --- /dev/null +++ b/src/astra/usecase/dashboard_request_receiver.py @@ -0,0 +1,272 @@ +from datetime import datetime +from queue import Queue +from typing import Iterable + +from astra.data.data_manager import DataManager +from astra.data.parameters import Tag +from astra.usecase.dashboard_handler import DashboardHandler +from astra.usecase.filters import DashboardFilters +from astra.usecase.request_receiver import RequestReceiver +from astra.usecase.table_return import TableReturn + +VALID_SORTING_DIRECTIONS = {'>', '<'} +VALID_SORTING_COLUMNS = {'TAG', 'DESCRIPTION'} +DATA = 'DATA' +CONFIG = 'CONFIG' +DASHBOARD_HEADINGS = ['TAG', 'DESCRIPTION'] + + +class DashboardRequestReceiver(RequestReceiver): + """ + DashboardRequestReceiver is a class that implements the RequestReceiver interface. + It handles requests from the dashboard, such as creating the initial data table, + updating the currently represented information, changing the index of the datatable + that we are viewing, adding or removing a tag from the set of tags that we are viewing, + and updating the sorting filter to be applied. + """ + + filters = DashboardFilters(set(), (">", "TAG"), + 0, datetime.min, datetime.max) + handler = DashboardHandler() + search_cache: dict[str, Iterable[str]] = dict() + search_eviction: Queue[str] = Queue() + _sorting = [-1, 1] + previous_data = None + + @classmethod + def create(cls, dm: DataManager) -> TableReturn: + """ + Create is a method that creates the initial data table, + with all tags shown, no sorting applied and at the first index. + + :param dm: Contains all data stored by the program to date. + """ + + all_tags = dm.tags + + # Add all tags to the shown tags by default. + if cls.filters.tags is None: + cls.filters.tags = set(all_tags) + + # Set the index to the first index by default. + if cls.filters.index is None: + cls.filters.index = 0 + + # Create the initial table. + cls.previous_data = cls.handler.get_data(dm, cls.filters) + return cls.previous_data + + @classmethod + def update(cls): + """ + update is a method that updates the currently represented information + """ + if cls.previous_data is not None: + cls.handler.update_data(cls.previous_data, cls.filters) + + @classmethod + def change_index(cls, index: int) -> bool: + """ + change_index changes the index of the datatable + that we are viewing. It returns True if it was successful and False otherwise. + + :param index: the index of the datatable that we want to change to. + :returns: True if the index was successfully changed and False otherwise. + """ + + cls.filters.index = index + + # Determine if we can update the view without issues. + if cls.filters.tags is None: + return False + return True + + @classmethod + def add_shown_tag(cls, add: str) -> bool: + """ + add_shown_tag is a method that adds a tag to the set of tags + that we are viewing. It returns True if it was successful and False otherwise. + + :param add: the tag that we want to add to the set of tags that we are viewing. + :returns: True if the tag was successfully added and False otherwise. + """ + if cls.filters.tags is None: + cls.filters.tags = set() + + # Determine if we can add the tag to the set of tags that we are viewing. + tag_index = add.index(':') + add_tag = add[:tag_index] + if add_tag not in cls.filters.tags: + cls.filters.tags.add(Tag(add_tag)) + return True + else: + # Tag was already in the set of tags that we are viewing. + return False + + @classmethod + def set_shown_tags(cls, tags: Iterable[Tag]): + """ + Sets to the set of tags to be shown + + PRECONDITION: is an element of + + :param tags: a set of tags to show + """ + cls.filters.tags = set(tags) + + @classmethod + def remove_shown_tag(cls, remove: str) -> bool: + """ + Remove a tag from the set of tags that we are viewing. + It returns True if it was successful and False otherwise. + + :param remove: The tag that we want to remove from the set of tags that we are viewing. + :return: True if the tag was successfully removed and False otherwise. + """ + if cls.filters.tags is None: + cls.filters.tags = set() + + # Determine if we can remove the tag from the set of tags that we are viewing. + tag_index = remove.index(':') + remove_tag = remove[:tag_index] + if remove_tag in cls.filters.tags: + cls.filters.tags.remove(Tag(remove_tag)) + return True + else: + return False # Tag was not in the set of tags that we are viewing. + + @classmethod + def toggle_sort(cls, heading: str) -> bool: + """ + Method for toggling sorting on a specific heading + The headings include (for now): + - TAG + - DESCRIPTION + This method will ask the model to sort the data + according to which heading was toggled + + Args: + heading (str): string representing which heading was toggled + """ + sort_value = -1 + for i in range(len(DASHBOARD_HEADINGS)): + check_heading = DASHBOARD_HEADINGS[i] + if heading == check_heading: + cls._sorting[i] *= -1 + sort_value = cls._sorting[i] + else: + cls._sorting[i] = 1 + + if sort_value == 1: + # ascending + cls.update_sort(('>', heading)) + elif sort_value == -1: + # descending + cls.update_sort(('<', heading)) + return sort_value == 1 + + @classmethod + def update_sort(cls, sort: tuple[str, str]) -> bool: + """ + Updates the sorting filter to be applied. + It returns True if the sorting filter was successfully applied and False otherwise. + + :param sort: the first value in the tuple for this key will + be either ">", indicating sorting by increasing values, + and "<" indicating sorting by decreasing values. The second + value will indicate the name of the column to sort by. + :returns: True if the sorting filter was successfully updated and False otherwise. + """ + + # Determine if the sorting filter is valid. + if sort[0] not in VALID_SORTING_DIRECTIONS: + return False + if sort[1] not in VALID_SORTING_COLUMNS: + return False + + # both if statements failed, so the filter is valid. + cls.filters.sort = sort + return True + + @classmethod + def set_start_time(cls, start_time: datetime): + """ + Modifies to be equal to + + :param start_time: the datetime to be set + """ + cls.filters.start_time = start_time + + @classmethod + def set_end_time(cls, end_time: datetime): + """ + Modifies to be equal to + + :param end_time: the datetime to be set + """ + cls.filters.end_time = end_time + + @classmethod + def search_tags(cls, search: str, dm: DataManager) -> Iterable[str]: + """ + Finds all tags in where is a substring + + :param search: The substring to search for + :param dm: The source of all data known to the program + :return: A list of all satisfying tags + """ + all_tags = dm.tags + if len(cls.search_cache) == 0: + all_params = dm.parameters + tag_strs = [] + for tag in all_tags: + param = all_params[tag] + tag_strs.append(tag + ": " + param.description) + tag_strs.sort() + cls.search_cache[''] = tag_strs + + return cls.handler.search_tags(search, cls.search_cache, cls.search_eviction) + + @classmethod + def get_num_frames(cls) -> int: + """ + Returns the number of frames from the last call to or + + :return: The number of telemetry frames in consideration + """ + if cls.previous_data is not None: + return cls.previous_data.frame_quantity + else: + return 0 + + @classmethod + def get_time(cls) -> datetime | None: + """ + Returns the time of the currently examined frame + + :return: The time of the currently examined frame, or None if no data is in the telemetry dashboard + """ + if cls.previous_data is not None: + return cls.previous_data.timestamp + return None + + @classmethod + def load_file(cls, file: str, dm: DataManager) -> None: + """ + Adds a telemetry file to the database + + :param dm: Stores all data known to the program + :param file: The file storing data to add + """ + + @classmethod + def get_table_entries(cls) -> list[list] | None: + """ + Returns the table entries from the previous call to or + + :return: the table entries from the previous call to or + """ + if cls.previous_data is None: + return None + else: + return cls.previous_data.table diff --git a/src/astra/usecase/request_receiver.py b/src/astra/usecase/request_receiver.py index ed8c959..6329a49 100644 --- a/src/astra/usecase/request_receiver.py +++ b/src/astra/usecase/request_receiver.py @@ -1,23 +1,17 @@ from abc import ABC, abstractmethod -from datetime import datetime -from queue import Queue from threading import Thread -from typing import Any, Iterable, Mapping +from typing import Any, Mapping from .alarm_checker import check_alarms -from .dashboard_handler import DashboardHandler, TableReturn, DashboardFilters from astra.data.data_manager import DataManager, Device -from ..data.parameters import Tag - -VALID_SORTING_DIRECTIONS = {'>', '<'} -VALID_SORTING_COLUMNS = {'TAG', 'DESCRIPTION'} -DATA = 'DATA' -CONFIG = 'CONFIG' -DASHBOARD_HEADINGS = ['TAG', 'DESCRIPTION'] class RequestReceiver(ABC): """ - RequestReceiver is an abstract class that defines the interface for front end data requests. + RequestReceiver is an abstract class that defines the interface for front end data processing + requests. + + :param previous_data: Stores the data last returned by the function + :type: Any """ previous_data: Any @@ -39,13 +33,22 @@ def update(self): class DataRequestReceiver(RequestReceiver): """ Receives new data files and updates our programs database accordingly. + + :param file: A file to read from for requests + :type: str + + :param previous_data: stores data manager last returned by this class' function + :type: DataManager """ - file = None - previous_data = None + file: str = "" + previous_data: DataManager | None = None @classmethod def set_filename(cls, file): + """ + setter method for + """ cls.file = file @classmethod @@ -66,17 +69,30 @@ def update(cls) -> None: """ if cls.previous_data is not None: - earliest_time = cls.previous_data.add_data_from_file(cls.file) + dm = cls.previous_data + filename = cls.file + earliest_time = dm.add_data_from_file(filename) checking_thread = Thread(target=check_alarms, args=[cls.previous_data, earliest_time]) checking_thread.start() @classmethod def data_exists(cls) -> bool: - return cls.previous_data.get_telemetry_data(None, None, {}).num_telemetry_frames > 0 + """ + Determines if the data manager has any stored data in it + + :return: True iff some telemetry data has been input to the DataManager + """ + + if cls.previous_data is not None: + return cls.previous_data.get_telemetry_data(None, None, {}).num_telemetry_frames > 0 + return False @classmethod def set_data_manager(cls, dm: DataManager) -> None: + """ + setter method for + """ cls.previous_data = dm @staticmethod @@ -104,261 +120,3 @@ def remove_device(device_name: str) -> None: :param device_name: The device to remove from the data manager """ DataManager.remove_device(device_name) - - -class DashboardRequestReceiver(RequestReceiver): - """ - DashboardRequestReceiver is a class that implements the RequestReceiver interface. - It handles requests from the dashboard, such as creating the initial data table, - updating the currently represented information, changing the index of the datatable - that we are viewing, adding or removing a tag from the set of tags that we are viewing, - and updating the sorting filter to be applied. - """ - - filters = DashboardFilters(set(), (">", "TAG"), - 0, datetime.min, datetime.max) - handler = DashboardHandler() - search_cache = dict() - search_eviction = Queue() - _sorting = [-1, 1] - previous_data = None - - @classmethod - def create(cls, dm: DataManager) -> TableReturn: - """ - Create is a method that creates the initial data table, - with all tags shown, no sorting applied and at the first index. - - :param model: The model of currently shown data - :param dm: Contains all data stored by the program to date. - """ - - all_tags = dm.tags - - # Add all tags to the shown tags by default. - if cls.filters.tags is None: - cls.filters.tags = set(all_tags) - - # Set the index to the first index by default. - if cls.filters.index is None: - cls.filters.index = 0 - - # Create the initial table. - cls.previous_data = cls.handler.get_data(dm, cls.filters) - return cls.previous_data - - @classmethod - def update(cls): - """ - update is a method that updates the currently represented information - """ - if cls.previous_data is not None: - cls.handler.update_data(cls.previous_data, cls.filters) - - @classmethod - def change_index(cls, index: int) -> bool: - """ - change_index changes the index of the datatable - that we are viewing. It returns True if it was successful and False otherwise. - - :param dm: The interface for getting all data known to the program - :param index: the index of the datatable that we want to change to. - :returns: True if the index was successfully changed and False otherwise. - """ - - cls.filters.index = index - - # Determine if we can update the view without issues. - if cls.filters.tags is None: - return False - return True - - @classmethod - def add_shown_tag(cls, add: str) -> bool: - """ - add_shown_tag is a method that adds a tag to the set of tags - that we are viewing. It returns True if it was successful and False otherwise. - - :param add: the tag that we want to add to the set of tags that we are viewing. - :param previous_table: the previous table that was in the view. - :returns: True if the tag was successfully added and False otherwise. - """ - if cls.filters.tags is None: - cls.filters.tags = set() - - # Determine if we can add the tag to the set of tags that we are viewing. - tag_index = add.index(':') - add_tag = add[:tag_index] - if add_tag not in cls.filters.tags: - cls.filters.tags.add(add_tag) - return True - else: - # Tag was already in the set of tags that we are viewing. - return False - - @classmethod - def set_shown_tags(cls, tags: Iterable[str]): - """ - Sets to the set of tags to be shown - - PRECONDITION: is an element of - - :param tags: a set of tags to show - """ - cls.filters.tags = set(tags) - - @classmethod - def remove_shown_tag(cls, remove: str) -> bool: - """ - Remove a tag from the set of tags that we are viewing. - It returns True if it was successful and False otherwise. - - :param previous_table: The previous table that was in the view. - :param remove: The tag that we want to remove from the set of tags that we are viewing. - :return: True if the tag was successfully removed and False otherwise. - """ - if cls.filters.tags is None: - cls.filters.tags = set() - - # Determine if we can remove the tag from the set of tags that we are viewing. - tag_index = remove.index(':') - remove_tag = remove[:tag_index] - if remove_tag in cls.filters.tags: - cls.filters.tags.remove(remove_tag) - return True - else: - return False # Tag was not in the set of tags that we are viewing. - - @classmethod - def toggle_sort(cls, heading: str) -> bool: - """ - Method for toggling sorting on a specific heading - The headings include (for now): - - TAG - - DESCRIPTION - This method will ask the model to sort the data - according to which heading was toggled - - Args: - heading (str): string representing which heading was toggled - """ - sort_value = -1 - for i in range(len(DASHBOARD_HEADINGS)): - check_heading = DASHBOARD_HEADINGS[i] - if heading == check_heading: - cls._sorting[i] *= -1 - sort_value = cls._sorting[i] - else: - cls._sorting[i] = 1 - - if sort_value == 1: - # ascending - cls.update_sort(('>', heading)) - elif sort_value == -1: - # descending - cls.update_sort(('<', heading)) - return sort_value == 1 - - @classmethod - def update_sort(cls, sort: tuple[str, str]) -> bool: - """ - Updates the sorting filter to be applied. - It returns True if the sorting filter was successfully applied and False otherwise. - - :param sort: the first value in the tuple for this key will - be either ">", indicating sorting by increasing values, - and "<" indicating sorting by decreasing values. The second - value will indicate the name of the column to sort by. - :returns: True if the sorting filter was successfully updated and False otherwise. - """ - - # Determine if the sorting filter is valid. - if sort[0] not in VALID_SORTING_DIRECTIONS: - return False - if sort[1] not in VALID_SORTING_COLUMNS: - return False - - # both if statements failed, so the filter is valid. - cls.filters.sort = sort - return True - - @classmethod - def set_start_time(cls, start_time: datetime): - """ - Modifies to be equal to - - :param start_time: the datetime to be set - """ - cls.filters.start_time = start_time - - @classmethod - def set_end_time(cls, end_time: datetime): - """ - Modifies to be equal to - - :param end_time: the datetime to be set - """ - cls.filters.end_time = end_time - - @classmethod - def search_tags(cls, search: str, dm: DataManager) -> list[Tag]: - """ - Finds all tags in where is a substring - - :param search: The substring to search for - :param dm: The source of all data known to the program - :return: A list of all satisfying tags - """ - all_tags = dm.tags - if len(cls.search_cache) == 0: - all_params = dm.parameters - tag_strs = [] - for tag in all_tags: - param = all_params[tag] - tag_strs.append(tag + ": " + param.description) - tag_strs.sort() - cls.search_cache[''] = tag_strs - - return cls.handler.search_tags(search, cls.search_cache, cls.search_eviction) - - @classmethod - def get_num_frames(cls) -> int: - """ - Returns the number of frames from the last call to or - - :return: The number of telemetry frames in consideration - """ - if cls.previous_data is not None: - return cls.previous_data.frame_quantity - else: - return 0 - - @classmethod - def get_time(cls) -> datetime: - """ - Returns the time of the currently examined frame - - :return: The time of the currently examined frame - """ - return cls.previous_data.timestamp - - @classmethod - def load_file(cls, file: str, dm: DataManager) -> None: - """ - Adds a telemetry file to the database - - :param dm: Stores all data known to the program - :param file: The file storing data to add - """ - - @classmethod - def get_table_entries(cls) -> list[list] | None: - """ - Returns the table entries from the previous call to or - - :return: the table entries from the previous call to or - """ - if cls.previous_data is None: - return None - else: - return cls.previous_data.table From 46809bc42f542238fecccf9d8b73522bcb3b8829 Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Tue, 5 Dec 2023 17:21:48 -0500 Subject: [PATCH 13/47] mypy fixes --- src/astra/data/alarm_container.py | 56 +++++++++++++++++------------- src/astra/frontend/tag_searcher.py | 24 ++++++------- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/astra/data/alarm_container.py b/src/astra/data/alarm_container.py index 8096496..5aa39db 100644 --- a/src/astra/data/alarm_container.py +++ b/src/astra/data/alarm_container.py @@ -11,10 +11,13 @@ class AlarmObserver: Observes the state of the global alarms container and notifies interested parties whenever an update occurs - :param watchers: A list of functions to call on any update to the alarm container - :param mutex: Synchronization tool as many threads may notify watchers of updates + :param: watchers: A list of functions to call on any update to the alarm container + :type: list[Callable] + + :param: mutex: Synchronization tool as many threads may notify watchers of updates + :type: Lock """ - watchers = [] + watchers: list[Callable] = [] _mutex = Lock() @classmethod @@ -41,14 +44,19 @@ class AlarmsContainer: A container for a global alarms dict that utilizes locking for multithreading :param alarms: The actual dictionary of alarms held + :type: dict[str, list[Alarm]] + :param mutex: A lock used for mutating cls.alarms + :type: Lock + :param observer: An Observer to monitor the state of the container + :type: AlarmObserver """ observer = AlarmObserver() - alarms = {AlarmPriority.WARNING.name: [], AlarmPriority.LOW.name: [], - AlarmPriority.MEDIUM.name: [], AlarmPriority.HIGH.name: [], - AlarmPriority.CRITICAL.name: []} - new_alarms = Queue() + alarms: dict[str, list[Alarm]] = {AlarmPriority.WARNING.name: [], AlarmPriority.LOW.name: [], + AlarmPriority.MEDIUM.name: [], AlarmPriority.HIGH.name: [], + AlarmPriority.CRITICAL.name: []} + new_alarms: Queue[Alarm] = Queue() mutex = Lock() @classmethod @@ -67,11 +75,10 @@ def add_alarms(cls, alarms: list[Alarm], """ Updates the alarms global variable after acquiring the lock for it - :param dm: Holds information of data criticality and priority :param apm: Maps information on alarms to correct priority level :param alarms: The set of alarms to add to """ - new_alarms = [] + new_alarms: list[tuple[Alarm, AlarmCriticality]] = [] times = [0, 5, 15, 30] timer_vals = [] alarms.sort(reverse=True) @@ -81,7 +88,7 @@ def add_alarms(cls, alarms: list[Alarm], with cls.mutex: for alarm in alarms: criticality = alarm.criticality - alarm_timer_vals = [] + alarm_timer_vals: list[timedelta] = [] cls.new_alarms.put(alarm) # Find the closest timeframe from 0, 5, 15, and 30 minutes from when the @@ -99,9 +106,9 @@ def add_alarms(cls, alarms: list[Alarm], if alarm.event.creation_time < endpoint_time and not alarm_timer_vals: priority_name = apm[timedelta(minutes=times[i - 1])][criticality] priority = AlarmPriority(priority_name) - cls.alarms[priority].append(alarm) + cls.alarms[priority.name].append(alarm) alarm.priority = priority - new_alarms.append([alarm, priority]) + new_alarms.append((alarm, priority)) remaining_time = endpoint_time - alarm.event.creation_time alarm_timer_vals.append(remaining_time) @@ -111,7 +118,7 @@ def add_alarms(cls, alarms: list[Alarm], priority = apm[timedelta(minutes=30)][criticality] alarm.priority = priority cls.alarms[priority.name].append(alarm) - new_alarms.append([alarm, priority]) + new_alarms.append((alarm, priority)) timer_vals.append(alarm_timer_vals) # Now that the state of the alarms container has been update, notify watchers @@ -119,7 +126,9 @@ def add_alarms(cls, alarms: list[Alarm], # Now, we need to create a timer thread for each alarm for i in range(len(new_alarms)): - alarm = new_alarms[i] + alarm_data: Alarm = new_alarms[i][0] + alarm_crit: AlarmCriticality = new_alarms[i][1] + associated_times = timer_vals[i] for associated_time in associated_times: @@ -128,29 +137,28 @@ def add_alarms(cls, alarms: list[Alarm], time_interval = len(times) - len(associated_times) + i new_timer = Timer(associated_time.seconds, cls._update_priority, - args=[alarm, timedelta(minutes=times[time_interval]), apm]) + args=[alarm_data, alarm_crit, timedelta(minutes=times[time_interval]), apm]) new_timer.start() @classmethod - def _update_priority(cls, alarm_data: list[Alarm, AlarmPriority], time: timedelta, + def _update_priority(cls, alarm: Alarm, alarm_crit: AlarmCriticality, time: timedelta, apm: Mapping[timedelta, Mapping[AlarmCriticality, AlarmPriority]]) -> None: """ Uses the alarm priority matrix with
from to . + Calculates the running average of the tag within the from to + + . :param data: The data to check - :param times: The times associated with (its keys) + :param times: The times associated with the data: :param start_date: The start date to check from - :param end_date: The end date to check to - :return: The rate of change of the tag within the from to . + :param time_window: The time_window to check over + :return: The running average starting at and ending at + . PRECONDITION: is in and are the keys of . """ @@ -294,11 +295,11 @@ def running_average_at_time(data: Mapping[datetime, ParameterValue | None], time curr_index += 1 - # If loop ended with as the same as , then we have roc = 0.0 + # If loop ended with as the same as , then we have running average = 0.0 if curr_time == start_date: return 0.0 - # otherwise we have the n-point running average. + # otherwise we can take the n-point running average. roc = total / (curr_index - initial_index) return roc @@ -308,8 +309,8 @@ def rate_of_change_check(dm: DataManager, alarm_base: RateOfChangeEventBase, criticality: AlarmCriticality, earliest_time: datetime, compound: bool, cv: Condition) -> list[bool]: """ - Checks if in the telemetry frames with times in the range - ( - -> present), there exists + Checks in the telemetry frames with times in the range + ( - -> present), if there exists a sequence lasting seconds where: a) reported a rate of change above b) reported a rate of change below @@ -329,7 +330,7 @@ def rate_of_change_check(dm: DataManager, alarm_base: RateOfChangeEventBase, # Calculating the range of time that needs to be checked first_time, sequence = find_first_time(alarm_base, earliest_time) - # Getting all the values relevant to this alarm. + # Getting all the important values to this alarm. tag = alarm_base.tag telemetry_data = dm.get_telemetry_data(first_time, None, [tag]) tag_values = telemetry_data.get_parameter_values(tag) @@ -342,15 +343,17 @@ def rate_of_change_check(dm: DataManager, alarm_base: RateOfChangeEventBase, # fall alarm, and 0 indicates no alarm. rising_or_falling_list = [] + # initialize the prev_running_average curr_running_average = 0.0 if len(times) > 0: prev_running_average = running_average_at_time(tag_values, times, times[0], alarm_base.time_window) else: prev_running_average = 0.0 - # Calculate the rate of change for each time window, and add the appropriate - # number to the list. - for start_date in times[1:-1]: + + # We calculate the running average at each time, and then calculate the rate of change + # by comparing adjacent running averages. + for start_date in times[1:]: # ROC is found by subtracting the previous running average from the current one. curr_running_average = running_average_at_time(tag_values, times, @@ -417,14 +420,16 @@ def rate_of_change_check(dm: DataManager, alarm_base: RateOfChangeEventBase, def repeat_checker(td: TelemetryData, tag: Tag) -> tuple[list[tuple[bool, datetime]], list[int]]: """ - Checks all the frames in and returns a list of tuples where each tuple - contains a boolean indicating if the value of is the same as the previous - frame, and the datetime associated with the frame. + Checks all the frames in and returns a 2-element tuple where the first element + is list of tuples where each tuple contains a date and boolean indicating whether the + alarm was active on that date. The second element of the tuple is a list of indices + where the first tuple element is False. :param tag: The tag to check the values of. :param td: The relevant telemetry data to check. - :return: A list of tuples where each tuple contains a boolean indicating if the value of - is the same as the previous frame, and the datetime associated with the frame. + :return: A 2-element tuple where the first element is list of tuples where each tuple + contains a date and boolean indicating whether the alarm was active on that date. + The second element of the tuple is a list of indices where the first tuple element is False. """ sequences_of_static = [] @@ -436,6 +441,7 @@ def repeat_checker(td: TelemetryData, tag: Tag) -> tuple[list[tuple[bool, dateti sequences_of_static.append((True, td.get_telemetry_frame(0).time)) values_at_times = td.get_parameter_values(tag) + # Iterate over each pair of timestamps and add a (True, datetime) to the list each time they # are the same or (False, datetime) when they aren't. for prev_time, curr_time in pairwise(values_at_times.keys()): @@ -483,8 +489,10 @@ def static_check(dm: DataManager, alarm_base: StaticEventBase, # Check which frames share the same value as the previous frame. cond_met, false_indexes = repeat_checker(telemetry_data, tag) + # Check for persistence and create alarms if necessary. alarm_indexes = persistence_check(cond_met, sequence, false_indexes) + # Loop through the alarm indexes and create alarms for each one. alarms = [] first_indexes = [] for index in alarm_indexes: From 9a1c0ad15e9dcb29971c54aaf18575453ef74255 Mon Sep 17 00:00:00 2001 From: shape-warrior-t Date: Wed, 6 Dec 2023 19:44:59 -0500 Subject: [PATCH 21/47] Correct some static typing errors in data subteam --- src/astra/data/database/db_initializer.py | 4 +- src/astra/data/database/db_manager.py | 109 ++++------------------ src/astra/data/telemetry_data.py | 2 +- 3 files changed, 20 insertions(+), 95 deletions(-) diff --git a/src/astra/data/database/db_initializer.py b/src/astra/data/database/db_initializer.py index 4c801f8..0c44c80 100644 --- a/src/astra/data/database/db_initializer.py +++ b/src/astra/data/database/db_initializer.py @@ -58,8 +58,8 @@ class Device(Base): __tablename__ = "Device" device_id: Mapped[int] = mapped_column(primary_key=True) - device_name: Mapped[str | None] = mapped_column(unique=True) - device_description: Mapped[str | None] = mapped_column() + device_name: Mapped[str] = mapped_column(unique=True) + device_description: Mapped[str] = mapped_column() # one-to-many relationship tags = relationship( diff --git a/src/astra/data/database/db_manager.py b/src/astra/data/database/db_manager.py index 91ed5f4..4899e4b 100644 --- a/src/astra/data/database/db_manager.py +++ b/src/astra/data/database/db_manager.py @@ -1,6 +1,7 @@ +from collections.abc import Sequence from datetime import datetime -from sqlalchemy import func +from sqlalchemy import func, Row from sqlalchemy import select, insert, update, delete from sqlalchemy.orm import sessionmaker @@ -12,6 +13,10 @@ Data, ) +# Docstring types have not been corrected in the latest change, +# since those will be going away anyway. +# TODO: put docstrings in the correct format + # auto initialize the database # an Engine, which the Session will use for connection @@ -159,7 +164,7 @@ def device_exists(device_name: str) -> bool: return get_device(device_name) is not None -def get_tags_for_device(device_name: str) -> list[tuple[str, dict]]: +def get_tags_for_device(device_name: str) -> Sequence[Row[tuple[str, dict]]]: """ return all tags for the given device Args: @@ -177,7 +182,7 @@ def get_tags_for_device(device_name: str) -> list[tuple[str, dict]]: return session.execute(select_stmt).all() -def get_alarm_base_info(device_name: str) -> list[tuple[str, dict]]: +def get_alarm_base_info(device_name: str) -> Sequence[Row[tuple[str, dict]]]: """ return all alarm info for the given device Args: @@ -195,7 +200,7 @@ def get_alarm_base_info(device_name: str) -> list[tuple[str, dict]]: return session.execute(select_stmt).all() -def get_tag_id_name(device_name: str) -> list[tuple[int, str]]: +def get_tag_id_name(device_name: str) -> Sequence[Row[tuple[int, str]]]: """ A helper function that fetches the tags for a device and converts that to a list of (tag_id, tag_name) @@ -244,100 +249,20 @@ def num_telemetry_frames( select_stmt = select_stmt.where(Data.timestamp >= start_time) if end_time is not None: select_stmt = select_stmt.where(Data.timestamp <= end_time) - return session.execute(select_stmt).scalar() + result = session.execute(select_stmt).scalar() + assert result is not None # The way the query is constructed, shouldn't ever be None + return result else: raise ValueError("Device does not exist in database") -# def get_timestamp_by_index( -# device_name: str, -# start_time: datetime | None, -# end_time: datetime | None, -# index: int, -# ) -> datetime: -# """ -# The th timestamp for a device between start_time and end_time -# May assume: 0 <= < -# Args: -# device_name (str): name of the device -# start_time (datetime | None): the start time of the data -# end_time (datetime | None): the end time of the data -# index (int): the index of the timestamp - -# Returns: -# datetime: the ith timestamp for the given device between start_time -# and end_time -# """ -# device = get_device(device_name) -# with Session.begin() as session: -# if device: -# device_name = device.device_name -# # tag_id_name = get_tag_id_name(device_name) -# # tag_ids = [tag_id for tag_id, _ in tag_id_name] -# select_stmt = ( -# select(Data.timestamp) -# .where( -# Device.device_name == device_name, -# ) -# .where( -# Tag.device_id == Device.device_id, -# ) -# .where( -# Tag.tag_id == Data.tag_id, -# ) -# .group_by(Data.timestamp) -# .order_by(Data.timestamp) -# .limit(index + 1) -# ) -# if start_time is not None: -# select_stmt = select_stmt.where(Data.timestamp >= start_time) -# if end_time is not None: -# select_stmt = select_stmt.where(Data.timestamp <= end_time) -# return session.execute(select_stmt).all()[-1][0] -# else: -# raise ValueError("Device does not exist in database") - - -# def get_telemetry_data_by_timestamp( -# device_name: str, tags: set[str] | None, timestamp: datetime -# ) -> list[tuple[str, float]]: -# """ -# All the data for the telemetry frame with the given timestamp for a device. -# May assume: timestamp is valid for the device -# Args: -# device_name (str): name of the device -# tags (set[str]): set of tags for the data to be returned -# timestamp (datetime): the timestamp of the data - -# Returns: -# list[tuple[str, float]]: a list of tuple (tag_name, value) for the given -# device/tags with the given timestamp -# """ -# device = get_device(device_name) -# with Session.begin() as session: -# if device: -# device_id = device.device_id -# select_stmt = ( -# select(Tag.tag_name, Data.value) -# .where(Data.tag_id == Tag.tag_id) -# .where(Tag.device_id == device_id) -# .where(Data.timestamp == timestamp) -# ) -# if tags is not None: -# select_stmt = select_stmt.where(Tag.tag_name.in_(tags)) - -# return session.execute(select_stmt).all() -# else: -# raise ValueError("Device does not exist in database") - - def get_telemetry_data_by_index( device_name: str, tags: set[str] | None, start_time: datetime | None, end_time: datetime | None, index: int, -) -> tuple[list[tuple[str, float]], datetime]: +) -> tuple[Sequence[Row[tuple[str, float | None]]], datetime]: """ All the data for the telemetry frame with the th timestamp for a device between start_time and end_time. @@ -374,7 +299,7 @@ def get_telemetry_data_by_index( sub_query = sub_query.filter(Data.timestamp >= start_time) if end_time is not None: sub_query = sub_query.filter(Data.timestamp <= end_time) - timestamp = sub_query.limit(1).offset(index).all() + timestamp = sub_query.limit(1).offset(index).scalar() query = ( session.query(Tag.tag_name, Data.value) @@ -382,14 +307,14 @@ def get_telemetry_data_by_index( .join(Tag.device) .filter( Device.device_name == device_name, - Data.timestamp == timestamp[0][0], + Data.timestamp == timestamp, ) ) if tags is not None: query = query.filter(Tag.tag_name.in_(tags)) - return (query.all(), timestamp[0][0]) + return query.all(), timestamp else: raise ValueError("Device does not exist in database") @@ -400,7 +325,7 @@ def get_telemetry_data_by_tag( end_time: datetime | None, tag: str, step: int = 1, -) -> list[tuple[float, datetime]]: +) -> Sequence[Row[tuple[float, datetime]]]: """ Every th data for the given tag for a device between start_time and end_time diff --git a/src/astra/data/telemetry_data.py b/src/astra/data/telemetry_data.py index 36b7c63..865d606 100644 --- a/src/astra/data/telemetry_data.py +++ b/src/astra/data/telemetry_data.py @@ -62,7 +62,7 @@ def tags(self) -> Iterable[Tag]: """The tags for this TelemetryData.""" return self._tags - def _convert_dtype(self, tag: Tag, value: float | None) -> ParameterValue: + def _convert_dtype(self, tag: Tag, value: float | None) -> ParameterValue | None: # Convert the float telemetry value from the database # to the correct type for the given parameter. if value is None: From f78c395b62d924f58597cb2e3a4892120de8d619 Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Wed, 6 Dec 2023 23:09:13 -0500 Subject: [PATCH 22/47] removed all pycache files --- src/astra/__pycache__/__init__.cpython-310.pyc | Bin 171 -> 0 bytes src/astra/__pycache__/__init__.cpython-311.pyc | Bin 171 -> 0 bytes src/astra/__pycache__/__init__.cpython-312.pyc | Bin 175 -> 0 bytes .../data/__pycache__/__init__.cpython-310.pyc | Bin 176 -> 0 bytes .../data/__pycache__/__init__.cpython-311.pyc | Bin 176 -> 0 bytes .../data/__pycache__/__init__.cpython-312.pyc | Bin 180 -> 0 bytes .../data/__pycache__/alarms.cpython-312.pyc | Bin 3953 -> 0 bytes .../__pycache__/config_manager.cpython-310.pyc | Bin 2901 -> 0 bytes .../__pycache__/config_manager.cpython-312.pyc | Bin 3859 -> 0 bytes .../__pycache__/data_manager.cpython-311.pyc | Bin 5853 -> 0 bytes .../__pycache__/data_manager.cpython-312.pyc | Bin 10895 -> 0 bytes .../__pycache__/dict_parsing.cpython-312.pyc | Bin 15965 -> 0 bytes .../data/__pycache__/parameters.cpython-312.pyc | Bin 1121 -> 0 bytes .../__pycache__/telemetry_data.cpython-312.pyc | Bin 6235 -> 0 bytes .../telemetry_manager.cpython-312.pyc | Bin 4116 -> 0 bytes .../__pycache__/__init__.cpython-310.pyc | Bin 180 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 184 -> 0 bytes .../frontend/__pycache__/model.cpython-310.pyc | Bin 2107 -> 0 bytes .../frontend/__pycache__/model.cpython-312.pyc | Bin 2256 -> 0 bytes .../frontend/__pycache__/view.cpython-310.pyc | Bin 10794 -> 0 bytes .../frontend/__pycache__/view.cpython-312.pyc | Bin 31382 -> 0 bytes .../view_draw_functions.cpython-310.pyc | Bin 2030 -> 0 bytes .../view_draw_functions.cpython-312.pyc | Bin 3124 -> 0 bytes .../__pycache__/view_model.cpython-310.pyc | Bin 3329 -> 0 bytes .../__pycache__/view_model.cpython-312.pyc | Bin 15519 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 183 -> 0 bytes .../__pycache__/alarm_checker.cpython-312.pyc | Bin 1540 -> 0 bytes .../__pycache__/alarm_handler.cpython-312.pyc | Bin 10422 -> 0 bytes .../alarm_strategies.cpython-312.pyc | Bin 34815 -> 0 bytes .../alarms_request_receiver.cpython-312.pyc | Bin 11665 -> 0 bytes .../dashboard_handler.cpython-312.pyc | Bin 10847 -> 0 bytes .../request_receiver.cpython-312.pyc | Bin 11187 -> 0 bytes .../use_case_handlers.cpython-312.pyc | Bin 3042 -> 0 bytes .../usecase/__pycache__/utils.cpython-312.pyc | Bin 2290 -> 0 bytes ...e_case_handlers.cpython-311-pytest-7.4.3.pyc | Bin 7658 -> 0 bytes ...e_case_handlers.cpython-312-pytest-7.4.3.pyc | Bin 19581 -> 0 bytes .../test_use_case_handlers.cpython-312.pyc | Bin 4615 -> 0 bytes 37 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/astra/__pycache__/__init__.cpython-310.pyc delete mode 100644 src/astra/__pycache__/__init__.cpython-311.pyc delete mode 100644 src/astra/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/astra/data/__pycache__/__init__.cpython-310.pyc delete mode 100644 src/astra/data/__pycache__/__init__.cpython-311.pyc delete mode 100644 src/astra/data/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/astra/data/__pycache__/alarms.cpython-312.pyc delete mode 100644 src/astra/data/__pycache__/config_manager.cpython-310.pyc delete mode 100644 src/astra/data/__pycache__/config_manager.cpython-312.pyc delete mode 100644 src/astra/data/__pycache__/data_manager.cpython-311.pyc delete mode 100644 src/astra/data/__pycache__/data_manager.cpython-312.pyc delete mode 100644 src/astra/data/__pycache__/dict_parsing.cpython-312.pyc delete mode 100644 src/astra/data/__pycache__/parameters.cpython-312.pyc delete mode 100644 src/astra/data/__pycache__/telemetry_data.cpython-312.pyc delete mode 100644 src/astra/data/__pycache__/telemetry_manager.cpython-312.pyc delete mode 100644 src/astra/frontend/__pycache__/__init__.cpython-310.pyc delete mode 100644 src/astra/frontend/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/astra/frontend/__pycache__/model.cpython-310.pyc delete mode 100644 src/astra/frontend/__pycache__/model.cpython-312.pyc delete mode 100644 src/astra/frontend/__pycache__/view.cpython-310.pyc delete mode 100644 src/astra/frontend/__pycache__/view.cpython-312.pyc delete mode 100644 src/astra/frontend/__pycache__/view_draw_functions.cpython-310.pyc delete mode 100644 src/astra/frontend/__pycache__/view_draw_functions.cpython-312.pyc delete mode 100644 src/astra/frontend/__pycache__/view_model.cpython-310.pyc delete mode 100644 src/astra/frontend/__pycache__/view_model.cpython-312.pyc delete mode 100644 src/astra/usecase/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/astra/usecase/__pycache__/alarm_checker.cpython-312.pyc delete mode 100644 src/astra/usecase/__pycache__/alarm_handler.cpython-312.pyc delete mode 100644 src/astra/usecase/__pycache__/alarm_strategies.cpython-312.pyc delete mode 100644 src/astra/usecase/__pycache__/alarms_request_receiver.cpython-312.pyc delete mode 100644 src/astra/usecase/__pycache__/dashboard_handler.cpython-312.pyc delete mode 100644 src/astra/usecase/__pycache__/request_receiver.cpython-312.pyc delete mode 100644 src/astra/usecase/__pycache__/use_case_handlers.cpython-312.pyc delete mode 100644 src/astra/usecase/__pycache__/utils.cpython-312.pyc delete mode 100644 tests/usecase/__pycache__/test_use_case_handlers.cpython-311-pytest-7.4.3.pyc delete mode 100644 tests/usecase/__pycache__/test_use_case_handlers.cpython-312-pytest-7.4.3.pyc delete mode 100644 tests/usecase/__pycache__/test_use_case_handlers.cpython-312.pyc diff --git a/src/astra/__pycache__/__init__.cpython-310.pyc b/src/astra/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 9a01c546cc0b4d110e51cecacaab65dbcbee502b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 171 zcmd1j<>g`kf@5<$Q$h4&5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!GtvQvM@wK6ZYL^m@pS+^vnxF|U$vACotF(y7f fGcU6wK3=b&@)n0pZhlH>PO2Tqo?<2-!NLFlnm8%} diff --git a/src/astra/__pycache__/__init__.cpython-311.pyc b/src/astra/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index eaeda75f2dae5ad34d3841aaf4206e2194c1eb55..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 171 zcmZ3^%ge<81cDsysUZ3>h=2h`DC095kTIPhg&~+hlhJP_LlF~@{~09t%g)6r#yLMF zGcP^HIoR3Qz%ZtuC_gJTxkT64M7J=tGB346H#09;wgjEsy$%s>_ZIFl^} diff --git a/src/astra/data/__pycache__/__init__.cpython-310.pyc b/src/astra/data/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index b381f0ce96facacd56cbbb59a90cc38922e90982..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176 zcmd1j<>g`kf@5<$Q$h4&5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;x_8vQvM@wK6ZYL^m@pS+^vnxF|U$vACotF(xIk kBrzsFJ~J<~BtBlRpz;=nO>TZlX-=vg$g*N4Ai=@_0EgTw`~Uy| diff --git a/src/astra/data/__pycache__/__init__.cpython-311.pyc b/src/astra/data/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 370ce88fb65409d1814b2081ab7a9038e8d4c589..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176 zcmZ3^%ge<81cDsysUZ3>h=2h`DC095kTIPhg&~+hlhJP_LlF~@{~09t%h|;$#yLMF zGcP^HIoR3Qz%ZtuC_gJTxkT64M7J=tGB346H#09;wPO4oIE6_}kt;PI6;sY}yBjX1K7*WIw6axUm-6<;o diff --git a/src/astra/data/__pycache__/__init__.cpython-312.pyc b/src/astra/data/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index dc5d11fa969148348ac70e36f00c43ae6e69515a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180 zcmX@j%ge<81jpuhrh@3lAOanHW&w&!XQ*V*Wb|9fP{ah}eFmxd6`X7p6Iz^FR2-9- zlayK%lU$r^Y+x8uP?VpQnp~o5Y=XqpEljP siI30B%PfhH*DI*}#bJ}1pHiBWYFESxw1W|di$RQ!%#4hTMa)1J07U>V00000 diff --git a/src/astra/data/__pycache__/alarms.cpython-312.pyc b/src/astra/data/__pycache__/alarms.cpython-312.pyc deleted file mode 100644 index 186982e0a1ccded9fe52c919b5e3000874b1f4fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3953 zcmbtX&2JM&6rcU_x8pd8lQD8A|oMj>U*v0CNU+8lq6k}P?s&aBr8-0y(TNB0%UK9|+r?st#NM*+`JjKFI{&blr?y6ifJ7JtV+0gJz5U9080df>Rm z2Ufh5=<%H=tjZ>3T`Q8B@#veV zs}rv0Xg}b&1`YT$&x7do98&_;#NAm64bUJBy8CHOMqA77)+ZTQ#cnJwv* zBnGz^c1}D^{XF#3#BXC$FNUUHBu>@UQ!ah%cYwYE4fr&VfOr>uv1eklmer=m(1G0H zjs5{Ncc}EP(llC8oPxI*1JQQBt{!u}MDzdhvRv!&lHqK+{jZos@SB=5(U3I)M zFt)3Xbss|5;Xn)|Xuzj=3`8derlc#TtOH|RGgUocYI?AS9Q$YS=$m6jD7J=GbZR_! zd-i&6DYwX#E6cZe;OfHs(#@+}y}Yz|nTO`CFRd)i&0g^#ihi>%P~cu=U!u5xqKD|` z*oI;OUQy5ABa!~DRjd(xfTaQr_%#1>K?@6>cs1b> z+TQm(3bzuExlc(Gw}AneNI2Zf1kZVf;u^d*9;mQn{pnpb-AHEI)ffT(&VLA&&JyA6 z3evB2PdJ>WH5E=5P0-+)pupx~0G==k-nF*~R=m4|EFH(-dE`LrIQLpsr1Tz8 MPmAI9@l#~kzZ^Fuc>n+a diff --git a/src/astra/data/__pycache__/config_manager.cpython-310.pyc b/src/astra/data/__pycache__/config_manager.cpython-310.pyc deleted file mode 100644 index d5056b1e7ed8e57816a7ecffbf1bd5d919213403..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2901 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 YtB?7fFN8>?{p4rDsTxm&US{Hd0KmEMK>z>% diff --git a/src/astra/data/__pycache__/data_manager.cpython-311.pyc b/src/astra/data/__pycache__/data_manager.cpython-311.pyc deleted file mode 100644 index 785711b7f234cf4598cf8f12c2a37e782233aacb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5853 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 diff --git a/src/astra/data/__pycache__/data_manager.cpython-312.pyc b/src/astra/data/__pycache__/data_manager.cpython-312.pyc deleted file mode 100644 index 0dcd39686c6fb991f88d9976b1027fd5affd760f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10895 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 diff --git a/src/astra/data/__pycache__/dict_parsing.cpython-312.pyc b/src/astra/data/__pycache__/dict_parsing.cpython-312.pyc deleted file mode 100644 index c1693a77b4be42254d25f2071ca0567731826029..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15965 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 diff --git a/src/astra/data/__pycache__/telemetry_manager.cpython-312.pyc b/src/astra/data/__pycache__/telemetry_manager.cpython-312.pyc deleted file mode 100644 index 4f796a19d506d5b18b96a461e9ed1d9076f4cb66..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4116 zcmb_fU2Gdw7QXZ2@xS9ZKjf!zCrwIXr*;#vfwUB8Nt%`x0^2_zxOk_Yn|OjfHZzmv z2U{wLhae$Q*oPYJBK)#aP%FV>9t%7y(mt%G){-!rmAXq`cr&C@)jsU*xnqy*cD4Jo zH_FVJbI(0L_nv#c<3D-5E(GHbe?EgHr~ zeA9CbyqOf6w$0hn+#Hv-&)GqS#SYPi(L5`1@3V7G(T<%GHRo#LJAm)T9?|*MFhYux zM%XR7u2WUJ6k4TM>5%&;@)@EGhbU-RlhSxL#2Bsxti?rqSxR7o!}5Y8W21%eG+`kw ziE&v-W5Y2orLakH3f7hsIZ;z>8X$o)@V^^{K(8UKKDS7l^AA085d8HTe*)#H93|4- zAd#nY^rE##A3c?5Vl&th*w(xOCefx1yoSkT{R&5(6OSKYR^$#&3^YP^ZO=)U2( zPBIck-UAlXlm+#$NfA%2QpgXhni4w9&o8A? z;Ea~!K^Zamu)UFHEgDz2BkuMW|w41Gn}MCtD2C`0?S_Y zUo~u(h13!b(S{vr^JUl)!)0=6bs9Jl3Bz!Z@=YrCxe%=c;P_yHoJ|R&%2zA9;Y|su z7EfnHXFnpal9g_8u%f}B3>~#2r8Ko z*ZQYT1mVRDR(Uz2@unC#1QPKjLB*jigH29kR}FRsPSmgonx?1iB%_fQ&tf*wN29uU#gH7X` zfZ`!iUP(@*mP9NPxKs@)u8@*!c=1XTTOpU#@K&RWLr@D^67)mHVa4z@^pmYJy0D<6 zBsloEAewruEHGR(M20vZ{suZLq|B%UR7iS0G#J2&F5gGpA9P=xESxPn`zz?M?Rlzr;db9Z@s!?ou;l33?i}9gjOv}y zjTOCfa^1bt+FLkXZXI6d40r3!p1nn`_>RtxZ}F2lKUwAvm-jrq{?eVuknAeRom!&s7-Y?Wu6+@KNffTR-&T)}gcdp|j;f=f4kL*tq~3Ra-guIw=nbZ z>@EXL5I#YjqrPuHw>fqjGzFQZ>X zxBbDwvhE++NZe)U5tu1Ft$T(*QSiW4->BXUO*^O{%3-y)cgxeSd-}J#2a0p0uF=gw68gToi1zv_F4VJU z{nSoRXyfs%@DV+Hq}=n&`swY#gTElk8`b^%Rj%OJ9*SIb6((=_VI^>H4^4cvs1F_6 z8al2I9pCo%>@xH)ad%Sp3^uzP-rQrlI|D&-=-^XdroKpBmqGA3-80hcSx4_qd(U>y zzU|K5?Z<)@x9J`S?)g#sfqQRLBpH4-Q{pZ(j%|IVzN6*9v6AQ5{as9<*3;Dee*`bw z-{oN6@BhM~j`P&LezK;TMh{eyZP$)@Pr8|}2R$by9AA&_0s0$H;KbA1H^DH_-#WY} zk>}eEJE8aZPI8`a!&6Vf#~;J}@chngJK1hG=;XvhO)aTJq+0BLPYj?OTCBIVz^Dyq zWoz786H95e))rdO8mL;ba=xkk7QX(k&@vB-fGBieh=@6czoRHy$N z`aYndKm7~Ftbx|(1N_?K1cCS@aa-n>WW16EYtRm8;8K)7ZhijrPgny0Xj8K(z+-Qm z;DK53pq?5G{6GF0|Uj`a$x+* z>D%@|$sYWnrKd1iZW$~&25+~tuCqIB{8rn3y={NFZ3wQtbN~9(w#R>Usxa{J3q@9M zAB1cD!8BZb=l(msu2S!@n}avi@93MMo4uv(m&(4`l4EukoIr*n9#0FB9FMPs2_O(i zKR7+})Y#mMFTHT+)%X*mPmD*#Mk8Zm@v(`>_-JHwG@6_Uc}>Js28af(+Oa8N@)0z# zB}!D8527zBUIH5?x|ofDiOtoPfB_^jfkinDtIZ7}9ubs}$G4V2oR`wM#%Dlpq)I`nULlOF8= diff --git a/src/astra/frontend/__pycache__/__init__.cpython-310.pyc b/src/astra/frontend/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index ff3cab0d7ee1b1db364544763f24337b0e5ea8bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180 zcmd1j<>g`kf@5<$Q$h4&5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;x_GvQvM@wK6ZYL^m@pS+^vnxF|U$vACotF($1j oKd&S;FC`{EJ~J<~BtBlRpz;=nO>TZlX-=vg$iiYKAi=@_0967l#sB~S diff --git a/src/astra/frontend/__pycache__/__init__.cpython-312.pyc b/src/astra/frontend/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index a5bf2c44763812f31c836950280fb6125ab892be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 184 zcmX@j%ge<81jpuhrh@3lAOanHW&w&!XQ*V*Wb|9fP{ah}eFmxd6`pJr6Iz^FR2-9- zlayK%lU$r^Y+x8uP?VpQnp~o5Y=XqpEljP;E2>-`rbHRY`BoI=I8l*^P73^_cm)b8wAROKbLsd zBjgY4j28rmw~03&U}3}(hg+kk&7HWRCDC3f33$Tnd%bDk0Ad}p>DX6})eTP!wi zMKP!9Fi`X`i}}0++`-O^Qi^Wby=NFJ+s@5YQ$s>HVq@w%mS*- z($Rd1wFZTo7LOV)(`XUEG_=u-WZz)~DRW8!q&;WX)g6$^M zO{j7Wpa&25+yFE+S{m)=h9W=0(=nITkSTKesn#=k&Jpnq<{`u4nmz~V?WB>4f;cMR zX)`Lbgi{fuEK8{3F;BP>1IhvwR2z=1(%e`rY6mpQ(ts8ADh(4H{OW2k8$_wTO&BQN z%fz6tS7=w^?B*eGquQ{`{A|DJ;{aa1n~hE zolu6WD_1&wkv-(0T3Wq2)?9kb2Ps!eQ5r6(juc@hkV*uduEZnS)_p&M zhx7dzI733P6W4JK{LLGUSH+ADfaTGma=IJEiVGhJiUn95q83I4+m&N~1~(spA|U9@ zN2a(0?yVdPZAUxVJ#}*BU+yWcWj8rMNk8{5K#pb~WiRFJ?B6(}QtG7@tL=MmmScne`3 z01ey_7j;W~1b-mRn|qsgcfQ%*xdTa6T*8$fkIg&xH*Vj%Ihy$dr{BhDOT3G1l&X$+ zt;Qj|4`XFb@Q_DO_&d;%4*)#JHAfP1U%4yZZxj8?-tQBA>b;ogU-15#=+An8PxR|W zb12HwbN`cCF}>>4iZ2HWB9JJJV!e9dZLUDVm7_CA;h3Z8Q}`h9CtNH_@QZftM7nGl z!|UTc|NRE_9#=Xi+=WSL0=V`lcZ{asWgVE8e2=zG3<(ZC)=KGpDU(?M;77 zs=BlGu!kISh=_QM$9U3^OOE~zUb0Bo(BLU}@HV=N5Dr4OK!r5+LB4gS#mN#VcgZGM$76e*bv&`>*)$#)dI?x8SOa3FpN=5F=Uyx`?q;x#z}w!^M+>LmJKA^;!J#RS>txlh)zwmb2Azwp$BZt%ZjN zerlb+V|_ItreWJ3`30)Rrtr04^1twr^>O|yLt2b68#-UUEW>}1LQbqDOc$gKqAUdH zx+r^D?59BzLpr57O?odSFecnG#GBJ36DqA}AZai7oU_w(gG2g>l&k=uUn(uvMUiC3KIJlec(emr?g+u{4)F2a6+xC$y z`^dI^V#_{pZ{@qI-(G!epLv;Yyh!nlf%-STmmy0A7fR1B@f6%G2~42cv3BVO^fnGn z>68S#y1l(vaexgWibEh?1ZZF5sZOL(S&Zk$@gQc1DvSNCuk$bRNA_U_LYdlC8UNia z{w@(L??TW^bP(r9LB+_nI@SQOMl z5-{kP1JE2Z=ty8VryBuSTIKY5oNUBs8CFYcnZ$-UPQW*%Q|>JGJ>f;l_99&Y525_@ zP0;VkcB@z)wPB5+G|(1UkmIt(NbD+WTkeU%c<$gyYy}X7X>xG3tXnWO0n2S?8arF; zMcj3ZhU)@DS%`Ymb#G)|INGVZE=zpZ)k%P@SHM4)h$$3Ux&rf1U@sIiC@>=hX7qic zLyql7)W-r{lAnUOO@1@Ystuj9x&PMElbP2ZS*@XERnLBZbVy+MVPyx0VbiF-b?==a zf#Kn~9UOLQ)#}1<+N#cea~#ji4x4q*m;WFz=sWScwqpwlV^K@P(7TX5SoHII|92X?!g35OKgajXepmDNo;V2`Sf27Ih+)Mr48Unvf7-;D V`GuVMnJoTQJ88`RPCzIF{{ZX{AIks$ diff --git a/src/astra/frontend/__pycache__/view.cpython-310.pyc b/src/astra/frontend/__pycache__/view.cpython-310.pyc deleted file mode 100644 index 18320fd5239e795e54d8d1d1a018bf99ea055ad8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10794 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 diff --git a/src/astra/frontend/__pycache__/view.cpython-312.pyc b/src/astra/frontend/__pycache__/view.cpython-312.pyc deleted file mode 100644 index cd0eb2c4e60f8ff2de50b61eb95894d87ce1e709..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31382 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 KHrzI&|NjD#TtqJb diff --git a/src/astra/frontend/__pycache__/view_draw_functions.cpython-310.pyc b/src/astra/frontend/__pycache__/view_draw_functions.cpython-310.pyc deleted file mode 100644 index b8ed819003c63b569364816d309c3298c887c433..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2030 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 G?f(HxN=I7& diff --git a/src/astra/frontend/__pycache__/view_model.cpython-312.pyc b/src/astra/frontend/__pycache__/view_model.cpython-312.pyc deleted file mode 100644 index 20c287376ab9025f3665efbc4bb711b373d764fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15519 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 GbNe48=dup~ diff --git a/src/astra/usecase/__pycache__/__init__.cpython-312.pyc b/src/astra/usecase/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 75afa7831d033579dcfc44999b114e3005292380..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 183 zcmX@j%ge<81jpuhrh@3lAOanHW&w&!XQ*V*Wb|9fP{ah}eFmxd6_#uj6Iz^FR2-9- zlayK%lU$r^Y+x8uP?VpQnp~o5Y=XqpEljPgjEsy$%s>_Z@u)CT diff --git a/src/astra/usecase/__pycache__/alarm_checker.cpython-312.pyc b/src/astra/usecase/__pycache__/alarm_checker.cpython-312.pyc deleted file mode 100644 index fcb1417ef80248b642c20589fe802fa26ec4d1d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1540 zcmZ8h&2Jk;6rWwM*K0eDNzz)SXx&lh{i!ts!+$sn(R*Pb@s!| zjuVX5QVtwk3Dk1xwO5oLdgM<);zHD-SSxaZOKzs&)Dv%ZZDJT{_x)zxyx;qnH}gv- zlR_}uPdDh6g3upgG8*a-oGt@+f(&Fx7P3(VNdn83WXly9aLiDwn5|S4z;Q#h;I1#jYDbNhUDK~8jSlwtaN{kaJQcct;uX&uohI;2j zjuz94gp*a`kUC|7Y}i4D5@wmy<2At&sEqP9bIQ8dKN=c<7<{J?5adutUD-#?k#Kr@>>=Pd2loJ!f)k_c;Z1MF5W@K?IPs!ch>ibqfTeMbVVCemgJL^6Z|s6+ z2oG9@;2p=^17=r{;}&!4Ks9a~1rpiUEq9w(c$YAfh_pQJGSjJtWO|!1>hNKzQDC31 z-&t5IFRpB?td&cnA~c8xITM{*9sDugp&iIw_}(Q}8|nwK?KMmt3dm>}$K2-a_3}!2 zX|90h?yhYLv3h55<=!fw<&~vn5DV+zV&V2(fFiGdDQwb$%t97|Ehuvc^Kzha!r)c} z3J9;9*MfAIW=&Mu3sQ6!G6^R^daT+^++ik~Hc&~cMI9rEd%y%dP1khtDJGhfi8mlf zKp(&z^qAGUiFw>CBjv;?NJ@P0|WG9E$l_g=3AX$BE+@m zu-PT>EN-FKgWoFC`=G~t5`}x_YpFF?*od2}-bMe>fU%WhjAB?o@qsfPpkH2}TUFmDn zN7{u$?ZR)`#X%N02h(Wg;wvP@b3L`MO+HFLOn;yGD+cpZhy~>}CLW+58Py1yFFKDm zqKt<1gl8-~t6)mB_`LYlhP}_k8x|(`BVv~Yo`v6Q?WXM-ZHwMwS70h$9j^$#K}?dQ lV|4i#T{=eZAEOTj>a3J|yfr`|`uM#;OvvK3zl99x{sB4rmXiPg diff --git a/src/astra/usecase/__pycache__/alarm_handler.cpython-312.pyc b/src/astra/usecase/__pycache__/alarm_handler.cpython-312.pyc deleted file mode 100644 index fbd3affacd7f501d0c75a7ae076752e621ea5eb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10422 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 aV6QBm5@x@9jZInDL(AiTWbjM3?tcMwJ#8NV diff --git a/src/astra/usecase/__pycache__/alarm_strategies.cpython-312.pyc b/src/astra/usecase/__pycache__/alarm_strategies.cpython-312.pyc deleted file mode 100644 index a186fbf244ae5925854e3e1f5923ad06553e7489..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34815 zcmeHwdvIGJka!XR0g&MP@+pE6Nzt@yiIyl+qGZXkWy^Y5^n-y2NFqgo@&%~Z zUasj(nox<7rDu0yda{$k+no$|w3#x0B*RVHRGsM`+BTiy3e*xtwGA_MJCm8t6l=MY znM|kc?>pz-iwh8vy&G@3ZhVKlk8{uap7VXb?{N2>*p1w8O35 zJo;g|AbcQ1gotTcm@$o+Omts1T{crbRz71MGtXGYEHf2j6*HA%l{3~c>x^y8He(;N z&p5^$GtM#RjBCs_Q#Dq_(v?rUXFOvbc5j~c&Qy<8vwO?5Z^l37p9zcw*mK2n%}niB zExWIr4$jn#)y)LQf_S!$)t3p;@`!N9#A_ICh}fc*h&@_;!5ne?hIy5En8bnwy z8oW?;p(0X+@TQ16+8k|(wp=KyL(+&RDn^6s&KqrwR3nWq>O>mz1!u&M_-&Cuv^G)` z^+#%>)sbK{5UGo{SdLv+XZW+6{)-RwzQ!Q?a>ys9R>B$@8U!u zV5%;p>WOTNdhiV@Sby^GAV$s9a=qPDLO0@W*GsUYSE*+oQzJ@^o(Lh$4mJPEg8X@Z z+9N%P(Hq&0ueAgJ_GkxE^&+f~h4txS{Rrzy_}NbZs&o z4LP%whpt9riKC-g%gA(Cn#tPO^MP~tg_<~R@PpDMG7e8qCT?Uc>}A$D61$;C zL)9Il;Y9f5a4dW=Dp5-ghNq{)=cl8gvTXUO@WpJ^t6?cT6HP>=GvVpED5AJdMW>?( zk#0~@#Iq$PXQGklbRwL!M$kff%$A)V$(qkot1F~vVor)ps8ocyOfcgg)J}Nxzj69N zkcI1}1n&3|F)icyJ=>djGzp0^E&L>&5RcNbqbOd57Ouv#YR^US7$v56YCPv-;#Fzk zYCMnjT$Eq67EbXJe(gz(Wu;amYP3*_V-pg={Bx-6=DT(g$IIcw#HDCl49CQ|*yY&l zwU~HeR+6@&Plx{Eess`C8$eqx}sTxXM zo2`ngBUKG_P+%|~eP=Ejn_!toX@GM(P&58KL;Ld?JuSl~~=$GEXrwzBg??i9JqKV$g*hFvQ&A2r2W|;cz z&AE7V0-b{#Fi*$TuAGd<`>x!`Ixj{OTF?!t17*@@j=zV~f{=3x4tK`YvF>X7iL2wG zZ(GW_Emtm7b#A(vZ;$`P)%?)cl5)0CP|K#Pb=}qS6Ibg)Ut7xAMqzE6E^*z}{1cb> z(AS!Bwo+K@rmNxho4@z=L*Mq4b327Je&VY8T|Bp^obBv+(^bWv9-$IK*3PTVrkE=l zmZnkj#5m2p34O*O9?dc~(|!*(G}ESVEj||935vsxi0OxA8poe6i@CV;9A1lZi`gDAVMaj>aw~F44SsVpavSB;T1stSW3Q()<&z zU7DP@R8WPPIn?qzCLv9ZXv8?#3ckaTcnY=Af_P=7Cu2yWP2*Vf8a|qs&#I*OV_CG-@^;2k%ah|Y+UTe^}rps3P2SRvGzlpYgd z7Po5J$Mhes7LBbRn6i~TNgU+gVfZ{!0Ix$9sS6ir8%|kQeC|9Z+XNq)S*u0|vUXN! zHellx#3T|ZO6`=!eE~yaJpW$mM`#HD@e?><{sjeV^+sirQrVQM?0gt(U1?np?pSnX zeEwV3Pw?2Y(eu30^Za^ngaVs3d?CdbTD0aWgzA=Llj7}LcroJ$EL~4IT2kf~sgnwI z@gabcqt~PHtcAu-JnM)=;}g>4mBi$1ENf?@Vw_Dm2SPR8Izjc<4x*!M!pXS7gfqzi z^cokt^h~J8Hi3mH9W3vYReJ2%8dmnR2CUPUy__r`t z)0n1{n2$85*h_VgQht;N7*{NUenasXY<$=PO5{glI5vy4l4#6`N6U|P!+7J9igP|> zh^$z1Ro2Ho;gI+eDn+dqXQc?Trn)i0Km>y7*$ZNk?+i8tsVgwUqu0YTlQC`jaeitT z5JTc98y)A1IyZcNb`G-|d9wjytWtgiaq2LK;U-PrV10JuN|cBe@2JG3a6+7&n3$6a zi#H5Ao&$|WadPI$tds~7fmB zf|4bg^M6C*G-Q?t6=f@9(d&uv$%sUQFk5x{_{hOy$6q=7(xK584vmJa(oO_R19W<- ze-$T?1yQJPS)N}xp|tcXbvsk$n$2L-^32MJ(%h#6`%~t?Cr0_KDdUjD^Yg zY)smYk_)Ck^$45({|z1XfpAfnx8T15Tv4Csh@l>`>5dvYUny5ksZ&>bOV>NsV@$ZO~%{wFI^DfysD!la@;JZ~I6jels=G|q2Tqe8a@;mfpDa0dt zq=0OX`y%FfyIhqZc*hRe{6mXY$D0V5x5=(~Ct^3sRkH0ez^zm(JK_PPbI6t{!g@uA z?37&>bVOkiWKVpz?2u5>MU(83orDQ)qSxlVAbDOQZu1qa{%A+6TDD7`h^Z)MHDXrl zG3|0isd6KF-=a)cq=LnF-TqCpfYPgPcm5@j{j1H}UD%WIITh02qFH zx|pOe3bZ#78CFT+UVT6f>wqR9&2jF3d^Q2le(Dlv8sxya=?DlmumDIBKx>5I5ZL}W-jI2$86@tCF| zFO^mr)9JYpgSapkn_z^W(HITFg~ju;v(r)V2ZC@SE02WDILk+y{P}D5s|Did4zcjL zuyk#f4i&`$L}KKf^(Ba1SY(nlk1$TgV?N{p}p4^B=XOFO2F zmc5OnRgMo`66D%R@M!j&bpz<4>f<`fb?cqTc~;f~jxMR#mUUIuG?}f^KQhn8Cxx#S zw<=pjV{M#u*f@Yr)=AW29C@(%xh{mK420T&((8 zh2X6Js6W%(w)je>vFC^9*1oacdrWCOw)k?UMNByxS%l@-s_TC9zK?xvnYOMaH>$DW zX;nO}srJFu6REam*FC%c+THZAyE!9vEMHymrh4}&p?ylr@RBp*X-YOKp57%_rfnON zwrzXg{jpcfw00#cR|Zo3`<3nc74dnzM#Lt?)2Bsr)@?XN#VIBO>&|Ux&*F(&6}L>e zXVILW4GGrTOt5k3YSNqP+@-YcQfi-BbY`r9tp zZ~PIyA;TL?X3vWi2H9Qbh<8-f0c5ZX{FWqg;i@2&0W*}#j%QOkMp9SwFx!)ZAGgN?2Y_yU~tojt-La;_tbRq|yhAxOt0xyFD z@_AOk+zNV)I1b4W98gbd5;E^nm0{{x>s<>53kE ze0G9lItcz6V`0tYgqUhjqFW5zarhLY!lCl4MItqJwjAmL=`e-{#Qrn5NM~`%I(dKb zgrwe6hp_tnq*oBBSnRK6Vn2_n4u0t^#QPq_y^Is6Q;h(^^irm_5me@(tNJ^GsoHI6 zUw7IS`c$a2Y+E#C9L^<|;%LgWKylXi*N%pixO3I3h=Z#m_r+&2&ftc#MRB$y%@3TN zoBEUGfwL>;Mg}>L;0&hBwR|R4FxFf+;Rt*)fPWV^9|-x0ChLZ>h>0n>${_i30~nM4 zjT7;Q<=PZvpt?K{?jxpBIch*16Sj>{SO)|PS&%I>LAicGF27uc?l;S26J;hSw@c+C zn|aM{cj!|wy>%+8u!Qt*;N8#^U|1O-kQkYRnwVsG8ZPIfh(Ut4aklV&GoVHuK;QuO z2&N*?P(g{|rU4M-ng=sXWH$_7h8L-3`ZzXd&(uk4s5HlIGjl*FEGLM(stSuWkBvG5 zn$y%}G##Ne;ce4q;3*xtv0bI7j4!8}*v) z<=r(*U~qUJt03St^!=>r#Rch~gK!{y31asQAhD^+@KUH;8pB9oNJu(|yHJJnHU)6p z0}Te`d7_TlYE}}l8v=g}kFeRAwXpYb8vh(ma4_Lzv$i1m1fu*o{^QqhS`a>~5vl`A zXTIIA{LcF=KoNBydh>5GsxBWt7Znh_9E%W6j%QzS3zZ)L5=; zg=(Q5zs9%OM*Loi+ zAaKn11+FA6V#Q37)0rg8n{g7(n2_5Ob7HCc#MhXCJ}$oT7Qw{WtQaRYsgvVX=zCbQK|Wk~W}t1&zH`(51ty-ZXXzvLlnp3tQ1DGkF;%32fi9 z0M&+0<=l=G1Dxeq3@ndW5<{t+`PuW#Pm0hhrmXJ6UDihZ$Hj06_Xce>=YkIQRaIVT zXK^`S1l?EpB5_BRHyQwlYVMvwwEu|z_;pR(cDbR1uCy=iOIOw{pSWMy3^P?ttV@V?{V!^Yl~ z3#*sYjRzn-JH6j}>DEijt;=tv>b9>tcjR^oPVd4?xn9BRTeLj%)_fZ@Bg5S1(aU|pRERFp3)y?LXB}b;IXJtTX>RmM{O#|=S!HU71d($=D_r2Yl&8Ww! z=?90D=BHLqD9z6-IUY8N$(lcSV`bpO@%Qa$L(7ih7zG(F**D$w%TsB0cc!U5c}!^{ znBFwFWY09VC5M#8zLj^B#(^bU#@D!f6XrWrH<0!nd)TpS^}^a4_v%w;&i=SL^~~#; zXGR{Cn>r7gGKXIM8E#IPmR{cU*O7K2xqW3m)w=J2e>fLH19LsB7dSYmf(k!6uKO@# z9fYpNuisQbB7yBx5A`-V8N5zU=RFHsC%B%?>7AgSRqsOgxb%;-iwCBBHHCe!4`yh`c` zLx#*C)Qfz?ypmaJtGa^0qH92L2G9TuF{OEoB$77ju z7@Dt8T>F5G%N@YX%nO-J<~()y2NlvmglEk(1SCvoE_P=9u{p?-n%0SHvg2HJv zFH?eP%Aoer1`2J1<^ zUhP{rmT3?_71}Iyi|$NA&&qzK;klGGNYZcPvUK0knh9=OJeq0gff6iJ7g~HV<7-oq zg5o>4XwA55m-pOvccivIw|0JG__Q*7I`#TFW%%6v?dLY@x>xFyy1~U4QJ||SX;PeU zQ)|own^Me>zIZg}5C@ZK<@~5b%F&*@nc8*e{`|OOE2FFZsqF_II1g^> zL5EiNr?wwlcaCOsEq%kfvo~Y)Exn^ygSQ;HLnic0?yyO4wkKasIR;YZ0hQ;25W;q& zP4utX{Qo01PnybKMK*u#SDDS9k50q(Otg4!;V}Mcu=&C|bJoddD`T3>r-pPAM*aq9 z!x?Y^tN$7(8sWR+$k?4?tVRs3CPlnz8i!$uZ|Gydb3nuhT?#Nb#T^5-s zVA3SPf<_CEuHwvPaf8a)l=o?S6D(%l0wua>3U|c^RNq1=iCnfoy0N(&`o|-;O%zt4{K{y!UQGTMG#J7;<;XSO_K9S#;b{^%bnu zj(KN-96;D1J4mY6+v7reAcaG_u$#%;Ra4}$U3AE9ZsyQhh1G!N^PWVl77dcRUG|Vi zzwV;!g#=zICvQHF+e2m{LFxr|K%3yuO&!&8HFO@PDJcDmkN5sG(WHf`y;3697w8q= zyx-6(KJT6_Uvsy&~Jb|;&){763nqlYDYSc6Fi zxxXWXQ&n@V%XhjP2jftXrkDdHffr_Wu#?Qxy;oZu!6d6;u~*#7?f>M}+-K09;?=OI zI#Gb_Ase!EFbfOIDV$eVlTMbD&yUSXv->j9c8Dhr6+|cP0<;n2Q%$}mMW94gw;1!+ zmQ58(eO#}7{^!b@W4uy%7cwZGx2}vT^w%FDcMHwsgKI)aL(j|zlkg{JZB`W_kr}wM zz}&zzEqvJ-^eyzA;QxOSaMP8vCEob}FL=v9@0x|k|XNV8DzL~eG{Ao$#9 zp=!2bhPgOqtI0!BZ}T{PW@#2vo9Sqnb|za>diYADu3$J!BC7?{)G(iW=9QCARX8vu z!c5G{B3;2<)-`v9beY`!F&?Uu7U+E!ooGn_UnNw*K6qRr3yjoDCvwj;`bAQ``7%L1 z2ANVv)%m#Cv?DJr<``Mc9#N~jKO`0&kglI6RW)8}m~PKuZj=r^O7uEgypXkFk(TPa z={01+%g>VAftIQ$8eLqn_c>4rm!Ik@oR5-oyq1isaGt8d|8CQ!Q(=urX` zBe6EIb~+t6xNtnfTsTSn)V$$+TJb);dNS?Zv*8_9yu)el^J(V@(?rnUJ5~Ag z!yV6Oob}5Gll2>|2b9(WcVFBX9aBcfV3g^7)1+8Ki>_2{=rgO(+`lrvb}4maEZy`* z%36=AG>f=q8hSq!EVjcwtf^_ckas6k1QU080<*CvD%sG+_~Db zb{W~$9WyN+-E>xe>*ZT7FNYpDJ8}-B|I8ya^{4tr?(R!Bo=92iG7UROCsx5$i~Wk% z6}5EMY&do(jvXn-?uUW;<>8HnVWnX>9oYY=U~;`?S~6w2o>_fI>DqI@>qKhb@q6zm z`%a`hV$yrx(+;y2q97A-0IIr8bH#?)ubBNyXV%S)8TibY8<_s9z;#sjWFr90T}J_f z%HhbVf>{hG{QxW$N-a>yJAqEA5L8csn*!sDFr^!+@^K7HV9a?BmeS^&VjvB8^SU7D zKp(~$X1v0BIJtt|G2<08KlvZqszSex&|~727y5s|Gp9r!C15b)2Y|8qo}1USCpf{5 zk>{Pd0M-)zUx>ySkP4)Tyc6@{1b{#lbBs5xdd;iuRnHNwqUTl{-H-!nsIJ%0{71-J zL36%SjR{=)0-Fl6r)WzL;?SLgpQ#uidUbMkjvKC6rF3Tl-Oy#^eY!q^#Vs=P{G28l zT6S`Z@FGkGw3OwsW}!k3o@d~X=^t|Q`{I4WGXLEE;UDbh>l-5E+?)0=U(Qe|9mep< z`U*KV>&dK{3=*7Y1A7bHtTKkXkS%XJ$@(;>jW{jh=PUM^wS?(PvNqtBs|;#0?MT+n zp4lj1nvyulwmN^|mW(RkV5|w{3X!NJ{Q;uCv`kkytf1Ki9>2xv z1~aYiEvasx1%M@7=w&l`^BL3)l?GKqWl2>-g+@!hkgCCIph=Z-g(g}unrWN28x#&U zcq-a;f!F~~;KV=7^{P+|rNI=0$>Kw#!JC`{>!hcNxnt^$BEr+^36H7B8C}54I6o#asF2c*RY9>pvk7eN2%p@(+Qn{Z; zo@8G4y4Sp6X@)9F)^M_~t|CcDlNYhIPc#y8=7mD(*D-O5q(Da4O9^_?KS5-Olna!m zP;M)d*QDQ~D5Q|jS~Y2myVvcc7r#lTGjuvnC$=>F_i(2z7U7sDP8+T0JV?Lcf(~f{ zB|4cfx8wg6H*hcS5*oMvoHEXnr5iA$mfn@O*G{IJM-+3z!jZ+9PyC(9ORLtk-n9S7 z!pocR**mN*k!vPT^;Wp3TLT+btp93UJ`LCQ*VnCGnVP;u$3s7skgZr&zOnY=JxAJq z3XXf$s&Bb&xt1m$SVeMKZ~e?E_`8xbtFNbh!&uu=gxzcP)hG_;jjFmMxaY8%5pD{) zB^w>0A9PPB;Su#EZTj*atranmbJa=VhAC7QCNBi07-B1f_8M|#iw^P*MXVS=L3Kl_ z0@aZrmFfsXo=rNaB(ii7t080gWxZ?$sEVhe1|WxXNKjOs(KJ|K3gr44apW=Rwy;If z>l4waw$4m%3XQG9QUkguR3`mXT%;sUaq2wHfzw}9Nhaw_-$RUEM0-RV5&;5L307aK zvN;2P#&N|BnR7?R;o<%UZWyqB?$3m*zu0HW8-!Ugzm(5Z)PP@w&-DE*pQ)&l{4@O# zqU}LnO8*QuPk?oQi8#M(SO-{Gfm#$D@UAqK^2waH0eaaJ4lO$NhR{VzEeyVsc_^sc zjq-1ch2?n$>15k`XA?S%gN>;oK{SFLum@N)dkzDLuK6m|!>wD3JhHo_UBpuY0(uSB zA`fg(-U0}SeH}{XRDDMWaRdS7DtMh*-~0Wd8q>7nHMSPj7&ac%I3W8}(CU!`CCk}b zWApr%Q)3&fGelB&{VJzOD_3-AZB(r@HSl(=A$5gno$>Q03N=_~{OAc`O1Jyuvjk|@ z`%HE+x?$O}AH`SKJ|l*=nok%P(akfA-)KItM#D)F?H;BL2%cn&VcDv=8O<;uw&PP@ z5seuxV1-{>QHYzWoSjtS93ty0K(YM;xfT;7FfPmHu~^6JOJef+n_qdTU0cLS)qwJi-CMw$K=JV`pg(q$O`{la9e3d|0;bAHf|nFKnJ1dp-E~=9MLpGJ_pXXBnC$4P^|XjA|%ns+qTh4|Xoi}Bk*2#t=UXh-YP|(#-{2I9Nkxb3rv(u6b&q))U~lU& zej2E3cdjSAFY1nC;(Mzsb_kEubER)bQd&--MbTz8oWnoX`>Fx}pc=X$b7V0V@Q zxuN6;Xw-7V2sj93U<2xcD$O?+)^Mt|sHu3An+<6j%);&On}sFQZL?X3Sg@IFeYz8oiBQ|V2Iv1ax$U9}{U%Ip(_-eYvZ%EXmiZQdRcrVn27;Z~pDM*^o8oDPRldjedD;mC z4p2;=z;tlfJD2>vNiW3BtZXM7%}qR#-$q>BINP`c+Gq(kEQh8N^k36p+w>$R!_$1L z@Iv<`Qt_RF{wh8@Crzk3b^iix)1y&rm<3;HzQwh^UYvD@sy{P0L=TCP=!NjybONd+ zeZP5KmBxC8{lbSax#k$n9DR75wV;2jAdzhd!=*mry1z?W0;jb>tPmnrZV6=jq)vT+<8?BUjSdlwSihwvsGNHr$1v$ zIC5;}UCAmSO;R;IhK#sZn-kmXPtMS`0JPlRITOB~f2z77&0+6-zKS0^@K*D68wJ^3 zqJBJ2c~@~q2rV{qHpm9p{ye-R64+j!zm>@8Al{DC76iUEfMaqIktI6)h*;PH@F_5P z^HxkwD#aa2%?@Z63w*0z``2fGbT%D0v~YaW?furZTh|tjUdIO02srNY>YmqU)82g>-UEvFK-xQ+b{>M~HFnk2{p~XK*!I$8d1?{A4AYXVO8fd3 ztse))WOefG)i>_;rh~6yGg>UXZ@_-DNM#LT-=C#-zT3V$ldj)^EqzHvdDMhmf12BY z<}YCnULUy=``WP^?uMsB@pL3ltb4X)oQ)gKb_Hwc-&!3|I(MzUo9cMsf%6Ew7m)lj zyHMAY>KR%)a(5;jJVjgEHf&hi6l+^@-vjGFt^pPNtWBu$vEBW8HtPG7`o45cf7-S4 z5q1aOvuJu)*S0dWfsJ^#@4stOwja1Vplm<1UUwM#20H>9juyp%9Rn3dTXH~gbYeCC z!=|p4n;ZQHmHvZwN0k1vw2UY%BX@h!Eicpd1`XRc8g?oTJ6GX#J+k=HmglCH#pCGx zZ9{9VYvCVtDqZkWw77;-p0-Rg?OoJ7h%J-ac5bwdDs7{8-$=Ke*l0VWw4K4O!~XW< z^nL$RnP7(!+_^e_S5EIbnen%6_(O_6l9qfJewAGuMH^T-juZ|6X;Hb zhLynnhfa6OgO__0&rsU=97?X)m)SQ=*XK=(_D{Bsr5$_&spo$C-DGQW>U(_|PxFSS zQ}J{rqYpegHua~-1J8CEJUcLU0k`0Ym~nCjjFfV#TA6XZ)h-qnT4QjVBg1XDNK#zjkA z_i4~=V!tYau3BxiD{2MZ3ZSW*3pVpU=tK=GFpW8AX|yZSRv~Rb_Fp$iR=`H99H27P zocy;v=nqfHUh@}L=FD&OBblwZqzI& z*OtsNSlAa>H*21+Gpu{tFdGw0$XzZKW8Nh65pYZs&eiFt6ktOYOupN65J}&f^rh zJ*wMa{X$FVSl$dPlyrM!uMGE{7*6B zx##Mi(H+C!hv{yXP6z1(g@O^6vt^46YrjP=pi&cl(D-D)E&YgMGDp^bNq7H>PJcls z=EM5ubVrz5`WJK})i3{Ppg67nV=$V5X6aRW6X1Y)CK{!mJJdGt&zhzTzOcVb`RKl| zOl=%*L>Nt~#~mD$lHNn2QzIB&{4c>R07}JQ1eEHalJI2tlksE`x~oKV01Hp`-w)MR zU|Y(Cqs*mN-PWCe8g{1i^`%|?w6}W)HiO2mD7^Dw5U^0}08n9@Sf(2K(zX3*?@qub z+v|j7JNDe~IGY+ik$UZ4)yvJ&srvra=2YGD>(-I1yF*_e zlJq{n&n;pn_>Rx~LZB--xf)#Cm-ZjUZ(QVkerxfw7Uc8m$ou@hfz3JJUWel+gH-VO zZCHr{g7@G@ru<{2e17fR=l5LNxBm$~zXh6ceI3fz0LQqh<*N>G;$H)tF9YB#mjpQ2 zd|TZz@<}-7F%U-sIbr;=rmC9Es~EoqIjTP&1B_oQA!*TUiCrETLgg!S4&SW9k8f*= zU;43zJjRj!l9>5_!08Jz|2B$Gr`L)3|GU2j^WUt;kEzr$Rnk|2^>>zJ{bx1Se z)zpdClzk^tr%o&T&Zw;a>^}hOAK-@YFNgIX4?P2IGL*>)+J;2JR~E8fY;7Zj`)DtW zJ~r**l9|Q<#D75|vo-q_Z}xlL64|CD?5!+FB>GC^3nh_Sfc=7}L`s2dtsy(gb9{#X z4!lH8QP~P;OtFuSM26RFzz~(Wr?DUDlW6ULL`s2dogqTeZXFBWZQ@p^$65`LG zmw_Kmrk@JUe_K{&I&b=0A%A|v?*C_nVA}apq2aHDT|X5D|4QiI^ad8HKDK+8T7Rc! zp)%+6n>v={;XMvvSJ+>579oxOMc$1Td}xeNmQ|K>$BeAM)+=@E;aJ6;wr1(T7L z!H*`X#=%Ue7f5$>t$X#j(sxh^jpoWJ^ytg?B0sKwMEAL!)ux)|NOISTf8{!U1+IO! z(!3`pAn=}rT~e=|engMCo~KOStq~DO5pl_MhF01?I+rV_`@v_|ENc^Y2iGqB$gK<> zc|?J^Dbuj&spYqF0xqjQcDWm6mrPeESB}t3|KRGOwT`>iwLazPqe}ma_eN7EPW||3 zs_)DrdY?N`_PmMbL08Izu9OR1SvKtEN%>HO+~Km5CR5|`8##flsb`My8*Ji{E2ogm z3$O5-O>DIixOHsVza06n9zPoQ$U?7k(`BNmZ)rX!;IcBvE~~rN{AqxZV+9#@7> zDtk_)&Wxqrcw0F$p4v064F1|9ii~}4L#E&o3dUt+mR&L!9OZb-?0Hy!RJ^R1yr z7KG(qH`z?yWGpA(a`&6;k{Wya5k2ODPk6Q0BrZF00xqe6gY24V9mtjAIdcHR23Nfp z+~>Y&vg7+Ae_Ym@+2!8oBYMi!ccQ}I$O*Wlc8;)XM%kU*SW*xdrZM)q{d|xiE+uaJ(Z)-%)w*qsy6{) ix$CB9d3;=(XlyBN=Qh^^TJVq3OTY|nOy9ocU2KsF(E zW_$eHG$(f5<-~5%{2;(=%=WG)l0Bc`OVX!DL_sBLp-7NUsYDi*B=U)mF{2rLOA~cX zE+zeXd|VRbqB0|tM2X1e>!pGOpDFNtTbjJKMiezo@}zK^$dDDC6jWhWCh@;-~ zYa-QsNvO$^hpMH(mw2dJO554pEk@8SDQzuzm|F^w+gkDyXep&#%iFjl-BwGuq`r9! zAxFM|{wTrKSBken8kDn|OC1+3mP56i&BQGu^_#3M!>CX#_;*@+tKcqZmaW0Lx|BId z;o~~h=-W$l-W9WR%ZA~fxaQj04XXJEPl<2L_~~6vgc(0EuRF(VGfU{L!aU7dx=C3- zAMVu&$pR+ztkCmY#DZMW=}eP#KlQt<>`MJq! zlM|`*+!C`FVfW(`r^av66^pv#1#5oXo#T;p<{*YVgrb*)PL}twwSYRwr4_HA8^XG3 z{E@Y6jvI(CI}V>s!q53Lh%)*nh9dhm!=sh(=-&hD;gjX5t)BR1&#_9+vAe_TJuj3m zd=o%wd=n2>@bCvWH}J?;c+Vz2RKbTk^%ezkb#nb8jqN)7(4=+|qo7bL;W=51ry zw15Ci`xYfX#08AA@O@72iKt)&wjbJp2-pGM;>c&iTpZ*=<6?x1Lm<3v-+hA$*kL}b z`tQnf&~8F6OK{yl3*ca_b;PAWf1c0z7z7}$`+tkL8eWf7@R3!rfye&c4uM;7Z8Ltf z5dkC$ieTz(XlYz$WXJyddr6))&{;M|{IP>9BVkIFW0Ex}LvDiigIEdy;W zpP3WfA-;QFN^Lo#{o-7spD;jjsqO5o>w4dVaL z8R8A!Wst}99?vd+b?*LZbL>oI?9BS;uh&LjT8sbYF-()#$2hlmfu$858gHNOxJ*y- zTuO+n>HmZB4@Cu2#*--Tpl?^piomp<_0%#V#&dQ**^6DeGWqK4^u_5*)3dL=k@Ech z*>e}CFTQeZHsvXh&UqnW#0TpJ^$F>g3F#)=w4Y~UQ)h2_;U%3x>glpVk$MVjL*!kI zKLz4!KSF|A#oMtHJY(8q*`P16bD-z1mh&f&RYj7df1m?jqUg6WmgENr#J3SelB*Ha zJ5rAQ3y*j>;o+o*$EuxbuUw8jh@jZO7Vay*`tFr?uDtj1I^O@mZ!7rV7Vh1`fi0|V zcd9YD9IAFVJe=_G$!G8oL80Dq03HoF`0mg From 37037e6cc88cd35f54c9302dd2ecdd1df8a1bf2b Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Wed, 6 Dec 2023 23:12:24 -0500 Subject: [PATCH 23/47] removed more unneccessary files --- tests/usecase/astra.db | Bin 28672 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/usecase/astra.db diff --git a/tests/usecase/astra.db b/tests/usecase/astra.db deleted file mode 100644 index c853a3e1957b431e68d09e4d9eade9973e607884..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28672 zcmeI%Z)?*)90%}Z>snXcS{x(v*)b4W9KsL;A8BlETeVAFnhJYV&gM2w`e#e8Q$Y}? zPkj%*AD{R>d;>o8E`LgcZGGaS`VGw`xl7XD?{mKtO4_GQAvrl8g##wZL!)e%rty>z z!!UAcEvPkJ^s!#k#Y}gWY;(qgH$S#Yzl{9t4@T*3>HF5V?VoqPssk(#fB*y_009U< z00I#B&jfC^^95^n*ZllZve$j?Fv;{)RO-lB3*B+@BHv_fP6#;3uK0Try zaa%rdJI&@kDQ-%3v2aKX_*k;RC2`bU{l*E+TzSp<6HX4BE!)>O>a(%z4n~1E7d)7M z#Y?YoVtZ%g1wA9xB&eFc_ORvA#*wQBs&POy;?YCuQMXRpWY1?8Dv@-gXnbie6jJn9UmF?A! z?JR73md=tqZ>x}J(K8TfB!5UuqsU@7QE)1Wnellf$Lj@aXUF{VHmVUts6M$YN9?+^ z2&P}MuzH4Uz~>6+`MPAG8kRi#3xYmZ1Q9{K<+gp#R>ewmZxX;{D6S^F+ZzqXGGt;X zNylxRc4%A{I);5RjrAW>-pl2!$2+DWh5>&!zEV@pyKEvy(Q#KHb`Mmu@#&FSuqqYv z<|uwDqj_?ob}j23cgD}=(o#z@nkClHOVSWA0uX=z1Rwwb2tWV=5P$##Ah4eB!Kt-CI>lg z|LV^%GK2sGAOHafKmY;|fB*y_009W#{Xc2|0uX=z1Rwwb2tWV=5P$##Ah7xZ`2WB9 abBqij009U<00Izz00bZa0SG_<0>1(Myj;Zq From 34aac8a06a8431df79be0a29a0220014778644f5 Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Wed, 6 Dec 2023 23:17:09 -0500 Subject: [PATCH 24/47] filled out gitignore --- .gitignore | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5a82abd..8a793ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,16 @@ -astra.db +# databases +*.db + +# pycache __pycache__ + +# mypy +.mypy_cache + +# pytest +.pytest_cache + +# pyinstaller +*.spec +dist/ +build/ \ No newline at end of file From b2007dee37db7f800bfc0a21c7c7ad82f5119d89 Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Wed, 6 Dec 2023 23:31:31 -0500 Subject: [PATCH 25/47] updated search test --- tests/usecase/test_use_case_handlers.py | 29 +++++-------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/tests/usecase/test_use_case_handlers.py b/tests/usecase/test_use_case_handlers.py index d65c96e..b85bea2 100644 --- a/tests/usecase/test_use_case_handlers.py +++ b/tests/usecase/test_use_case_handlers.py @@ -233,29 +233,12 @@ def test_search_tags_eviction(): cache[''] = tags.copy() eviction = queue.Queue() - DashboardHandler.search_tags('abcde', cache, eviction) - DashboardHandler.search_tags('a', cache, eviction) - DashboardHandler.search_tags('b', cache, eviction) - DashboardHandler.search_tags('c', cache, eviction) - DashboardHandler.search_tags('d', cache, eviction) - DashboardHandler.search_tags('e', cache, eviction) - DashboardHandler.search_tags('f', cache, eviction) - DashboardHandler.search_tags('g', cache, eviction) - DashboardHandler.search_tags('h', cache, eviction) - DashboardHandler.search_tags('i', cache, eviction) - DashboardHandler.search_tags('j', cache, eviction) - DashboardHandler.search_tags('k', cache, eviction) - DashboardHandler.search_tags('l', cache, eviction) - DashboardHandler.search_tags('m', cache, eviction) - DashboardHandler.search_tags('n', cache, eviction) - DashboardHandler.search_tags('o', cache, eviction) - DashboardHandler.search_tags('p', cache, eviction) - DashboardHandler.search_tags('q', cache, eviction) - DashboardHandler.search_tags('r', cache, eviction) - DashboardHandler.search_tags('s', cache, eviction) + for i in range(200): + search = 'a' * (i + 1) + DashboardHandler.search_tags(search, cache, eviction) check = DashboardHandler.search_tags('t', cache, eviction) assert check == [] - assert len(cache) == 21 - # The function already evicted abcde, so 'a' should be the next one - assert eviction.get() == 'a' + assert len(cache) == 201 + # The function already evicted 'a', so 'aa' should be the next one + assert eviction.get() == 'aa' From bc442c41dae4e3bd2870e38b650b30346b72d632 Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Wed, 6 Dec 2023 23:39:30 -0500 Subject: [PATCH 26/47] flake8 fixes --- src/astra/frontend/graphing_view.py | 3 ++- src/astra/usecase/alarm_strategies.py | 9 ++++++--- src/astra/usecase/filters.py | 2 -- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/astra/frontend/graphing_view.py b/src/astra/frontend/graphing_view.py index bb1a10b..c27e6fb 100644 --- a/src/astra/frontend/graphing_view.py +++ b/src/astra/frontend/graphing_view.py @@ -153,7 +153,8 @@ def export_data(self) -> None: try: self.controller.export_data_to_file(config_path) except Exception as e: - messagebox.showerror(title='Could not save data', message=f'{type(e).__name__}: {e}') + messagebox.showerror(title='Could not save data', message=f'{type(e).__name__}: ' + f'{e}') def create_graph(self) -> None: """ diff --git a/src/astra/usecase/alarm_strategies.py b/src/astra/usecase/alarm_strategies.py index 0379e72..8ac693a 100644 --- a/src/astra/usecase/alarm_strategies.py +++ b/src/astra/usecase/alarm_strategies.py @@ -282,7 +282,8 @@ def running_average_at_time(data: Mapping[datetime, ParameterValue | None], time :param times: The times associated with the data: :param start_date: The start date to check from :param time_window: The time_window to check over - :return: The running average starting at and ending at + . + :return: The running average starting at and ending at + + . PRECONDITION: is in and are the keys of . """ @@ -701,7 +702,8 @@ def forward_checking_propagator(first_events: list[tuple[int, datetime]], return satisfying_events -def get_smallest_domain(events: list[Union[list[tuple[int, datetime]], tuple[int, datetime]]]) -> int: +def get_smallest_domain(events: list[Union[list[tuple[int, datetime]], tuple[int, datetime]]]) \ + -> int: """ Returns the index of the event with the smallest remaining domain values that isn't already assigned @@ -855,7 +857,8 @@ def sequence_of_events_check(dm: DataManager, alarm_base: SOEEventBase, Condition()) if not inner_alarm_indexes: - # If any nested alarm was not raised, sequence of events couldn't have happened, so return early + # If any nested alarm was not raised, sequence of events couldn't have happened, + # so return early with cv: cv.notify() return [False] * len(inner_alarm_indexes) diff --git a/src/astra/usecase/filters.py b/src/astra/usecase/filters.py index a26d20c..48a38b2 100644 --- a/src/astra/usecase/filters.py +++ b/src/astra/usecase/filters.py @@ -1,7 +1,5 @@ from dataclasses import dataclass from datetime import datetime - -from astra.data.alarms import AlarmPriority, AlarmCriticality from astra.data.parameters import Tag From c3e31f6456e95b39a5178e0361203bf764d37518 Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Wed, 6 Dec 2023 23:43:51 -0500 Subject: [PATCH 27/47] removed unused constants --- src/astra/usecase/dashboard_request_receiver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/astra/usecase/dashboard_request_receiver.py b/src/astra/usecase/dashboard_request_receiver.py index 3211600..dca61a7 100644 --- a/src/astra/usecase/dashboard_request_receiver.py +++ b/src/astra/usecase/dashboard_request_receiver.py @@ -11,8 +11,6 @@ VALID_SORTING_DIRECTIONS = {'>', '<'} VALID_SORTING_COLUMNS = {'TAG', 'DESCRIPTION'} -DATA = 'DATA' -CONFIG = 'CONFIG' DASHBOARD_HEADINGS = ['TAG', 'DESCRIPTION'] @@ -244,8 +242,10 @@ def get_time(cls) -> datetime | None: """ Returns the time of the currently examined frame - :return: The time of the currently examined frame, or None if no data is in the telemetry dashboard + :return: The time of the currently examined frame, or None if no data is in the + telemetry dashboard """ + if cls.previous_data is not None: return cls.previous_data.timestamp return None From 214a714fe8f39639776d206b4caf2e712eb01e19 Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Thu, 7 Dec 2023 11:22:39 -0500 Subject: [PATCH 28/47] fixed mypy issues --- src/astra/usecase/alarm_strategies.py | 41 ++++++++++++++++----------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/astra/usecase/alarm_strategies.py b/src/astra/usecase/alarm_strategies.py index 8ac693a..d993b2d 100644 --- a/src/astra/usecase/alarm_strategies.py +++ b/src/astra/usecase/alarm_strategies.py @@ -16,6 +16,7 @@ next_id = EventID(0) + def get_strategy(base: EventBase) -> Callable: """ Matches an unknown form of EventBase to the correct strategy to check them @@ -27,6 +28,7 @@ def get_strategy(base: EventBase) -> Callable: :param base: An EventBase to evaluate :return: The function needed to evaluate """ + match base: case RateOfChangeEventBase(): return rate_of_change_check @@ -58,7 +60,7 @@ def find_first_time(alarm_base: EventBase, earliest_time: datetime) -> tuple[dat # Calculating the range of time that needs to be checked if alarm_base.persistence is None: subtract_time = timedelta(seconds=0) - sequence = 0 + sequence = 0.0 else: subtract_time = timedelta(seconds=alarm_base.persistence) sequence = alarm_base.persistence @@ -90,7 +92,7 @@ def create_alarm(alarm_indexes: tuple[int, int], times: list[datetime], event = Event(event_base, next_id, register_timestamp, confirm_timestamp, datetime.now(), event_base.description) - next_id += 1 + next_id = EventID(next_id + 1) # Note: priority needs to be determined externally, so we temporarily set it to provided # criticality @@ -115,6 +117,7 @@ def check_conds(td: TelemetryData, tag: Tag, condition: Callable, confirmed. Also returns a list of booleans where each index i refers to whether or not the i-th frame of had an active alarm. """ + alarm_data = [] alarm_indices = [] tag_values = td.get_parameter_values(tag) @@ -353,7 +356,6 @@ def rate_of_change_check(dm: DataManager, alarm_base: RateOfChangeEventBase, rising_or_falling_list = [] # initialize the prev_running_average - curr_running_average = 0.0 if len(times) > 0: prev_running_average = running_average_at_time(tag_values, times, times[0], alarm_base.time_window) @@ -557,7 +559,6 @@ def threshold_check(dm: DataManager, alarm_base: ThresholdEventBase, :param criticality: The base criticality of the alarm :param earliest_time: The earliest time from a set of the most recently added telemetry frames - :param all_alarms: Container for the list of all alarms :param compound: If this algorithm is being called as part of a compound alarm :param cv: Used to notify completion of this task :return: A list of all alarms that should be newly raised, and a list of bools @@ -609,7 +610,7 @@ def threshold_check(dm: DataManager, alarm_base: ThresholdEventBase, with cv: cv.notify() return all_alarm_frames - + return [] def setpoint_cond(param_value: ParameterValue, setpoint: ParameterValue) -> bool: """ @@ -758,22 +759,22 @@ def backtracking_search(events: list[Union[list[tuple[int, datetime]], tuple[int if chosen_domain == -1: # indicates solution was found - first_event: int = 0 - last_events: int = 0 - if type(events[0][0]) is int: - # this should always pass - first_event = events[0][0] + # these assertion checks should always pass + assert type(events[0][0]) is int + first_event = events[0][0] - if type(end_events[-1][-1][0]) is int: - # this should always pass - last_events = end_events[-1][-1][0] + assert type(end_events[-1][-1]) is tuple + last_events = end_events[-1][-1][0] return [first_event, last_events] - first_event = end_events[chosen_domain] - second_event = events[chosen_domain + 1] + first_event_list = end_events[chosen_domain] + assert type(first_event_list) is list + + second_event_list = events[chosen_domain + 1] + assert type(second_event_list) is list - events[chosen_domain + 1] = forward_checking_propagator(first_event, second_event, + events[chosen_domain + 1] = forward_checking_propagator(first_event_list, second_event_list, time_window[chosen_domain]) # updating the end events to match the new domain of the chosen variable @@ -781,9 +782,14 @@ def backtracking_search(events: list[Union[list[tuple[int, datetime]], tuple[int for updated_event in events[chosen_domain + 1]: for i in range(len(end_events[chosen_domain + 1])): curr_end_events = end_events[chosen_domain + 1][i] + + assert type(updated_event) is tuple + assert type(curr_end_events) is tuple + if curr_end_events[0] > updated_event[0]: updated_end_events.append(curr_end_events) break + end_events[chosen_domain + 1] = updated_end_events if not events[chosen_domain + 1]: @@ -799,10 +805,13 @@ def backtracking_search(events: list[Union[list[tuple[int, datetime]], tuple[int # For each element in the chosen domain, we assign one and check if a solution # to the sequence of events exists event = events[chosen_domain][i] + assert type(event) is tuple + new_events[chosen_domain] = event new_end_events[chosen_domain] = end_events[i] return backtracking_search(new_events, new_end_events, time_window) + return [] def pad_alarm_indexes(inner_alarms: list[list[bool]], max_size) -> list[list[bool]]: From 0522ad5dcaf754422000ea15be9b83b2f5527a91 Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Thu, 7 Dec 2023 11:35:03 -0500 Subject: [PATCH 29/47] fixed mypy errors --- src/astra/data/config_manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/astra/data/config_manager.py b/src/astra/data/config_manager.py index a17c8e8..77dd17a 100644 --- a/src/astra/data/config_manager.py +++ b/src/astra/data/config_manager.py @@ -38,7 +38,7 @@ def _read_config_yaml(filename: str) -> None: create_update_alarm(dictionary_of_alarms, device_id=device) -def yaml_tag_parser(tag_dict: dict) -> dict: +def yaml_tag_parser(tag_dict: dict) -> Parameter: """ A helper function for yaml reader that parse the tag data into a dictionary. @@ -60,6 +60,8 @@ def yaml_tag_parser(tag_dict: dict) -> dict: else: setpoint = None + dtype: type[int] | type[float] | type[bool] + if tag_data["dtype"] == "int": dtype = int elif tag_data["dtype"] == "float": From 7a39763f4ec06d4b8e0f677575ec363c6d0d1154 Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Thu, 7 Dec 2023 12:19:38 -0500 Subject: [PATCH 30/47] updated documentation and commenting --- src/astra/frontend/telemetry_view.py | 162 +++++++++++++++------- src/astra/frontend/view.py | 3 +- src/astra/frontend/view_draw_functions.py | 15 +- 3 files changed, 120 insertions(+), 60 deletions(-) diff --git a/src/astra/frontend/telemetry_view.py b/src/astra/frontend/telemetry_view.py index a3ea144..17656e2 100644 --- a/src/astra/frontend/telemetry_view.py +++ b/src/astra/frontend/telemetry_view.py @@ -16,47 +16,62 @@ class TelemetryView: + """Contains the GUI elements of the Telemetry Dashboard""" + def __init__(self, frame: ttk.Notebook, num_rows: int, dm: DataManager): + """ + Initializes all GUI elements of the telemetry dashboard + + :param frame: The notebook of tabs which this view should be inserted into + :param num_rows: Used for scaling to fullscreen + :param dm: Contains all data known to the program + """ self.dm = dm self.overall_frame = Frame(frame) self.controller = DashboardRequestReceiver() self.data_controller = DataRequestReceiver() + # Configuring row/column weightings for i in range(num_rows): self.overall_frame.grid_rowconfigure(i, weight=1) self.overall_frame.grid_columnconfigure(0, weight=1) self.overall_frame.grid_columnconfigure(1, weight=2) + # Configuring search widget self.dashboard_searcher = TagSearcher(num_rows, self.overall_frame, self.dm, self.dashboard_searcher_update) - add_data_button = Button(self.overall_frame, text='Add data...', command=self.open_file) + # Configuring button to add new data + add_data_button = Button(self.overall_frame, text='Add data...', command=self._open_file) add_data_button.grid(sticky='W', row=0, column=1) + # Configuring timerange input for changing available telemetry frames TimerangeInput( - self.overall_frame, 'Time range', self.update_dashboard_times + self.overall_frame, 'Time range', self._update_dashboard_times ).grid(sticky='W', row=1, column=1) self.dashboard_current_frame_number = 0 self.navigation_text = StringVar(value='Frame --- at ---') + # Configuring column header for changing telemetry frame and seeing current frame + # information self.navigation_row_outside = Frame(self.overall_frame) self.navigation_row_outside.grid(sticky='ew', row=2, column=1, pady=(10, 0)) self.navigation_row = Frame(self.navigation_row_outside) self.navigation_row.pack(expand=False) Button(self.navigation_row, text='|<', - command=self.first_frame).grid(sticky='w', row=0, column=0) + command=self._first_frame).grid(sticky='w', row=0, column=0) Button(self.navigation_row, text='<', - command=self.decrement_frame).grid(sticky='w', row=0, column=1) + command=self._decrement_frame).grid(sticky='w', row=0, column=1) (Label(self.navigation_row, textvariable=self.navigation_text) .grid(sticky='ew', row=0, column=2)) Button(self.navigation_row, text='>', - command=self.increment_frame).grid(sticky='e', row=0, column=3) + command=self._increment_frame).grid(sticky='e', row=0, column=3) Button(self.navigation_row, text='>|', - command=self.last_frame).grid(sticky='e', row=0, column=4) + command=self._last_frame).grid(sticky='e', row=0, column=4) - # dashboard table + # Configuring dashboard table style = ttk.Style() style.theme_use('clam') style.configure('Treeview.Heading', background='#ddd', font=('TkDefaultFont', 10, 'bold')) @@ -75,20 +90,20 @@ def __init__(self, frame: ttk.Notebook, num_rows: int, dm: DataManager): dashboard_table.column('setpoint', anchor=CENTER, width=80) dashboard_table.column('units', anchor=CENTER, width=80) dashboard_table.column('alarm', anchor=CENTER, width=80) - dashboard_table.heading('tag', text='Tag ▼', anchor=CENTER, command=self.toggle_tag) + dashboard_table.heading('tag', text='Tag ▼', anchor=CENTER, command=self._toggle_tag) dashboard_table.heading('description', text='Description ●', anchor=CENTER, - command=self.toggle_description) + command=self._toggle_description) dashboard_table.heading('value', text='Value', anchor=CENTER) dashboard_table.heading('setpoint', text='Setpoint', anchor=CENTER) dashboard_table.heading('units', text='Units', anchor=CENTER) dashboard_table.heading('alarm', text='Alarm', anchor=CENTER) - dashboard_table.bind('', self.double_click_dashboard_table_row) + dashboard_table.bind('', self._double_click_dashboard_table_row) - dashboard_table.bind('', self.move_row_up) - dashboard_table.bind('', self.move_row_down) + dashboard_table.bind('', self._move_row_up) + dashboard_table.bind('', self._move_row_down) self.data_controller.set_data_manager(self.dm) - self.toggle_tag() + self._toggle_tag() self.dashboard_searcher.update_searched_tags() if self.data_controller.data_exists(): @@ -96,14 +111,13 @@ def __init__(self, frame: ttk.Notebook, num_rows: int, dm: DataManager): self.controller.change_index(0) self.controller.create(self.dm) - self.refresh_data_table() + self._refresh_data_table() self.dashboard_searcher.select_all_tags() - def toggle_tag(self) -> None: + def _toggle_tag(self) -> None: """ - This method is the toggle action for the tag header - in the dashboard table + Defines what occurs when the "tag" table header is clicked """ ascending = self.controller.toggle_sort('TAG') if ascending: @@ -113,12 +127,11 @@ def toggle_tag(self) -> None: self.dashboard_table.heading('tag', text='Tag ▼') self.dashboard_table.heading('description', text='Description ●') self.controller.update() - self.refresh_data_table() + self._refresh_data_table() - def toggle_description(self) -> None: + def _toggle_description(self) -> None: """ - This method is the toggle action for the description header - in the dashboard table + Defines what occurs when the "Description" table header is clicked """ ascending = self.controller.toggle_sort('DESCRIPTION') if ascending: @@ -128,9 +141,9 @@ def toggle_description(self) -> None: self.dashboard_table.heading('description', text='Description ▼') self.dashboard_table.heading('tag', text='Tag ●') self.controller.update() - self.refresh_data_table() + self._refresh_data_table() - def double_click_dashboard_table_row(self, event) -> None: + def _double_click_dashboard_table_row(self, event) -> None: """ This method specifies what happens if a double click were to happen in the dashboard table @@ -139,31 +152,34 @@ def double_click_dashboard_table_row(self, event) -> None: region = self.dashboard_table.identify('region', event.x, event.y) if cur_item and region != 'heading': - self.open_telemetry_popup(self.dashboard_table.item(cur_item)['values']) + self._open_telemetry_popup(self.dashboard_table.item(cur_item)['values']) - def refresh_data_table(self) -> None: + def _refresh_data_table(self) -> None: """ This method wipes the data from the dashboard table and re-inserts the new values """ if self.controller.get_table_entries() is not None: - self.change_frame_navigation_text() + self._change_frame_navigation_text() for item in self.dashboard_table.get_children(): self.dashboard_table.delete(item) for item in self.controller.get_table_entries(): self.dashboard_table.insert('', END, values=tuple(item)) - def construct_dashboard_table(self): + def construct_dashboard_table(self) -> None: + """ + Makes a request to obtain all data pertaining to the current telemetry frame and + reconstructs the shown data table + """ self.controller.create(self.dm) - self.refresh_data_table() + self._refresh_data_table() - def open_telemetry_popup(self, values: list[str]) -> None: + def _open_telemetry_popup(self, values: list[str]) -> None: """ This method opens a new window to display one row of telemetry data - Args: - values (list[str]): the values to be displayed in the - new window + :params: values (list[str]): the values to be displayed in the + new window """ new_window = Toplevel(self.overall_frame) new_window.title('Telemetry information') @@ -171,7 +187,7 @@ def open_telemetry_popup(self, values: list[str]) -> None: for column in values: Label(new_window, text=column).pack() - def open_file(self): + def _open_file(self) -> None: """ This method specifies what happens when the add data button is clicked @@ -182,6 +198,7 @@ def open_file(self): return try: + # Uses the input file and makes a request to insert new data self.data_controller.set_filename(file) self.data_controller.update() self.controller.create(self.dm) @@ -189,76 +206,109 @@ def open_file(self): messagebox.showerror(title='Cannot read telemetry', message=f'{type(e).__name__}: {e}') return - self.refresh_data_table() + self._refresh_data_table() self.dashboard_searcher.update_searched_tags() - def update_dashboard_times( + def _update_dashboard_times( self, start_time: datetime | None, end_time: datetime | None ) -> OperationControl: + """ + + :param start_time: The minimum requested time for telemetry frames + :param end_time: The maximum requested time for telemetry frames + :return: An operation code indicating how to proceed + """ if self.dm.get_telemetry_data(start_time, end_time, {}).num_telemetry_frames == 0: messagebox.showinfo( title='No telemetry frames', message='The chosen time range does not have any telemetry frames.' ) return OperationControl.CANCEL + + # Making a backend request to change the minimum/maximum time and update the table self.controller.set_start_time(start_time) self.controller.set_end_time(end_time) self.dashboard_current_frame_number = 0 self.controller.change_index(0) self.controller.create(self.dm) - self.change_frame_navigation_text() + self._change_frame_navigation_text() - self.refresh_data_table() + self._refresh_data_table() return OperationControl.CONTINUE - def first_frame(self): + def _first_frame(self) -> None: + """ + Sets the current frame to the first available telemetry frame + """ if self.controller.get_num_frames() == 0: return self.dashboard_current_frame_number = 0 self.controller.change_index(0) self.controller.create(self.dm) - self.refresh_data_table() + self._refresh_data_table() - def last_frame(self): + def _last_frame(self) -> None: + """ + Sets the current frame to the last available telemetry frame + """ if self.controller.get_num_frames() == 0: return last = self.controller.get_num_frames() - 1 self.dashboard_current_frame_number = last self.controller.change_index(last) self.controller.create(self.dm) - self.refresh_data_table() + self._refresh_data_table() - def decrement_frame(self): + def _decrement_frame(self) -> None: + """ + Sets the current frame to the previous available telemetry frame, if any exist + """ + # Case for no available frames if self.controller.get_num_frames() == 0: return + + # Ensures nothing occurs when on the first available frame if self.dashboard_current_frame_number > 0: self.dashboard_current_frame_number -= 1 + index = self.dashboard_current_frame_number self.controller.change_index(index) self.controller.create(self.dm) - self.refresh_data_table() + self._refresh_data_table() + + def _increment_frame(self) -> None: + """ + Sets the current frame to the next available telemetry frame, if any exist + """ - def increment_frame(self): if self.controller.get_num_frames() == 0: return last = self.controller.get_num_frames() - 1 + + # Ensures nothing occurs when on the last available frame if self.dashboard_current_frame_number < last: self.dashboard_current_frame_number += 1 index = self.dashboard_current_frame_number self.controller.change_index(index) self.controller.create(self.dm) - self.refresh_data_table() + self._refresh_data_table() - def move_row_up(self, event: Event): + def _move_row_up(self, event: Event): + """ + Moves the currently selected row up one row + """ focus_item = self.dashboard_table.focus() region = self.dashboard_table.identify('region', event.x, event.y) + + # Ensuring the current focus is a table row if focus_item and region != 'heading': focus_row = self.dashboard_table.selection() index = self.dashboard_table.index(focus_item) if len(focus_row) == 1 and index > 0: + # If focusing the first available row, nothing happens self.dashboard_table.move(focus_row[0], self.dashboard_table.parent(focus_row[0]), index - 1) @@ -267,7 +317,10 @@ def move_row_up(self, event: Event): self.dashboard_table.focus(prev_row) self.dashboard_table.selection_set(prev_row) - def move_row_down(self, event: Event): + def _move_row_down(self, event: Event) -> None: + """ + Moves the currently selected row down one row + """ focus_item = self.dashboard_table.focus() region = self.dashboard_table.identify('region', event.x, event.y) @@ -284,7 +337,11 @@ def move_row_down(self, event: Event): self.dashboard_table.focus(prev_row) self.dashboard_table.selection_set(prev_row) - def change_frame_navigation_text(self): + def _change_frame_navigation_text(self) -> None: + """ + Changes the table header to account for changes in number of available frame + or change in examined frame + """ curr = self.dashboard_current_frame_number + 1 total = self.controller.get_num_frames() time = self.controller.get_time() @@ -292,11 +349,16 @@ def change_frame_navigation_text(self): f'Frame {curr}/{total} at {time}' ) - def dashboard_searcher_update(self): + def dashboard_searcher_update(self) -> None: + """ + Watcher function to be called once any update occurs in the search widget + """ # Convert to list to enforce ordering selected_tags = list(self.dashboard_searcher.selected_tags) + + # changing available tags self.controller.set_shown_tags(selected_tags) if self.controller.get_num_frames() > 0: self.controller.update() - self.refresh_data_table() + self._refresh_data_table() diff --git a/src/astra/frontend/view.py b/src/astra/frontend/view.py index 9f531a2..da25494 100644 --- a/src/astra/frontend/view.py +++ b/src/astra/frontend/view.py @@ -15,7 +15,7 @@ class View(Tk): """ - View class + The container class for all GUI elements of the main screen """ _dm: DataManager @@ -51,6 +51,7 @@ def __init__(self, device_name: str) -> None: self.graphing_tab = GraphingView(tab_control, height // 4, width, self._dm) graphing_frame = self.graphing_tab.overall_frame + # setting up observers for alarm updates watchers = [ self.telemetry_tab.construct_dashboard_table, self.alarm_tab.construct_alarms_table, diff --git a/src/astra/frontend/view_draw_functions.py b/src/astra/frontend/view_draw_functions.py index 58b8a17..54e7022 100644 --- a/src/astra/frontend/view_draw_functions.py +++ b/src/astra/frontend/view_draw_functions.py @@ -9,9 +9,8 @@ def draw_status(status_frame, descriptions): """ Draws the status frame of the dashboard - Args: - status_frame: frame of the status - descriptions: content to be displayed in the frame + :param: status_frame: frame of the status + :param: descriptions: content to be displayed in the frame """ # Wipe the frame first @@ -31,9 +30,8 @@ def draw_warnings(warnings_frame, descriptions): Draws the warnings frame of the dashboard Assumes warnings are sorted by critical first - Args: - warnings_frame: frame for warnings - descriptions: informations for what to go in the warnings + :param: warnings_frame: frame for warnings + :param: descriptions: informations for what to go in the warnings """ # Wipe the frame first @@ -63,9 +61,8 @@ def draw_frameview(frameview_frame, descriptions): Takes a tkinter Frame, and a 2d array of label strings Can be called whenever an update is required - Args: - frameview_frame (_type_): tkinter frame - descriptions (_type_): list of lists to display + :param frameview_frame: tkinter frame to insert descriptions into + :param descriptions: 2D list of label strings to display """ # Wipe the frame first From 88c2ecadf37dfeb656a8c067856b068376487efa Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Thu, 7 Dec 2023 13:38:05 -0500 Subject: [PATCH 31/47] fixed bug and documented alarm view --- src/astra/frontend/alarm_view.py | 222 +++++++++++++------ src/astra/usecase/alarms_request_receiver.py | 23 +- 2 files changed, 173 insertions(+), 72 deletions(-) diff --git a/src/astra/frontend/alarm_view.py b/src/astra/frontend/alarm_view.py index a6e25ff..d25d609 100644 --- a/src/astra/frontend/alarm_view.py +++ b/src/astra/frontend/alarm_view.py @@ -10,27 +10,38 @@ from astra.frontend.timerange_input import OperationControl, TimerangeInput from .tag_searcher import AlarmTagSearcher from ..data.alarms import Alarm +from ..data.parameters import Tag from ..usecase.alarms_request_receiver import AlarmsRequestReceiver class AlarmView: + """Contains the GUI elements of the Alarm Tab""" def __init__(self, frame: ttk.Notebook, num_rows: int, dm: DataManager): + """ + Initializes the elements of the Alarm Tab + + :param frame: The notebook of tabs which this view should be inserted into + :param num_rows: Used for scaling to fullscreen + :param dm: Contains all data known to the program + """ self.dm = dm self.overall_frame = Frame(frame) self.controller = AlarmsRequestReceiver() + # Configuring row and column weightings for i in range(num_rows): self.overall_frame.grid_rowconfigure(i, weight=1) self.overall_frame.grid_columnconfigure(0, weight=0) self.overall_frame.grid_columnconfigure(1, weight=1) + # Configuring search widget self.alarms_searcher = AlarmTagSearcher(num_rows, self.overall_frame, self.dm, self.alarms_searcher_update) - # alarms filters (for the table) + # alarms filter options (for the table) alarms_tag_table = Frame(self.overall_frame) style = ttk.Style() style.theme_use('clam') @@ -39,50 +50,56 @@ def __init__(self, frame: ttk.Notebook, num_rows: int, dm: DataManager): self.dangers = ['WARNING', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] self.types = ['RATE_OF_CHANGE', 'STATIC', 'THRESHOLD', 'SETPOINT', 'SOE', 'L_AND', 'L_OR'] + # Acknowledgement filter button = Button(alarms_tag_table, text='Show new alarms only', relief='raised', - command=lambda: self.flick_new()) + command=lambda: self._flick_new()) button.grid(sticky='w', row=0, columnspan=8) self.alarms_button_new = button + # Priority filter self.alarms_buttons_priority = [] label = Label(alarms_tag_table, text='Filter priority:') label.grid(sticky='news', row=1, column=0) for i in range(len(self.dangers)): button = Button(alarms_tag_table, text=self.dangers[i], relief='sunken', - command=lambda x=i: self.flick_priority(x)) + command=lambda x=i: self._flick_priority(x)) button.grid(sticky='news', row=1, column=i + 1) self.alarms_buttons_priority.append(button) + # Criticality filter label = Label(alarms_tag_table, text='Filter criticality:') label.grid(sticky='news', row=2, column=0) self.alarms_buttons_criticality = [] for i in range(len(self.dangers)): button = Button(alarms_tag_table, text=self.dangers[i], relief='sunken', - command=lambda x=i: self.flick_criticality(x)) + command=lambda x=i: self._flick_criticality(x)) button.grid(sticky='news', row=2, column=i + 1) self.alarms_buttons_criticality.append(button) + # Register time filter TimerangeInput( - alarms_tag_table, 'Registered', self.update_alarm_registered_times + alarms_tag_table, 'Registered', self._update_alarm_registered_times ).grid(sticky='W', row=3, columnspan=8) + # Confirm time filter TimerangeInput( - alarms_tag_table, 'Confirmed', self.update_alarm_confirmed_times + alarms_tag_table, 'Confirmed', self._update_alarm_confirmed_times ).grid(sticky='W', row=4, columnspan=8) + # Alarm type filter label = Label(alarms_tag_table, text='Filter type:') label.grid(sticky='news', row=5, column=0) self.alarms_buttons_type = [] for i in range(len(self.types)): button = Button(alarms_tag_table, text=self.types[i], relief='sunken', - command=lambda x=i: self.flick_type(x)) + command=lambda x=i: self._flick_type(x)) button.grid(sticky='news', row=5, column=i + 1) self.alarms_buttons_type.append(button) - # alarms table + # Configuring alarms table alarms_table_frame = Frame(self.overall_frame) alarms_table_frame.grid(sticky='NSEW', row=2, column=1, rowspan=num_rows - 3) style = ttk.Style() @@ -109,106 +126,161 @@ def __init__(self, frame: ttk.Notebook, num_rows: int, dm: DataManager): alarms_table.column('Description', anchor=CENTER, width=200) alarms_table.heading('ID', text='ID ▲', anchor=CENTER, - command=lambda: self.sort_alarms('ID')) + command=lambda: self._sort_alarms('ID')) alarms_table.heading('Priority', text='Priority ●', anchor=CENTER, - command=lambda: self.sort_alarms('PRIORITY')) + command=lambda: self._sort_alarms('Priority')) alarms_table.heading('Criticality', text='Criticality ●', anchor=CENTER, - command=lambda: self.sort_alarms('CRITICALITY')) + command=lambda: self._sort_alarms('Criticality')) alarms_table.heading('Registered', text='Registered ●', anchor=CENTER, - command=lambda: self.sort_alarms('REGISTERED')) + command=lambda: self._sort_alarms('Registered')) alarms_table.heading('Confirmed', text='Confirmed ●', anchor=CENTER, - command=lambda: self.sort_alarms('CONFIRMED')) + command=lambda: self._sort_alarms('Confirmed')) alarms_table.heading('Type', text='Type ●', anchor=CENTER, - command=lambda: self.sort_alarms('TYPE')) + command=lambda: self._sort_alarms('Type')) alarms_table.heading('Parameter(s)', text='Parameter(s)', anchor=CENTER) alarms_table.heading('Description', text='Description', anchor=CENTER) - alarms_table.bind('', self.double_click_alarms_table_row) + alarms_table.bind('', self._double_click_alarms_table_row) self.controller.toggle_sort('ID') self.alarms_searcher.update_searched_tags() if self.dm.get_telemetry_data(None, None, {}).num_telemetry_frames > 0: - self.refresh_alarms_table() + self._refresh_alarms_table() self.alarms_searcher.update_searched_tags() self.alarms_searcher.select_all_tags() - def sort_alarms(self, tag: str): - headers = ['ID', 'Priority', 'Criticality', 'Registered', 'Confirmed', 'Type'] - ascending = self.controller.toggle_sort(heading=tag) + def _sort_alarms(self, tag: str) -> None: + """ + Sorts the alarms table by the header - if tag != 'ID': - header_name = tag[0] + tag[1:].lower() - else: - header_name = tag + :param tag: The table header to sort by + """ + headers = ['ID', 'Priority', 'Criticality', 'Registered', 'Confirmed', 'Type'] + ascending = self.controller.toggle_sort(heading=tag.upper()) - if header_name in headers: + if tag in headers: + # Changing the header text to indicate sort if ascending: - self.alarms_table.heading(header_name, text=header_name + ' ▲') + self.alarms_table.heading(tag, text=tag + ' ▲') else: - self.alarms_table.heading(header_name, text=header_name + ' ▼') + self.alarms_table.heading(tag, text=tag + ' ▼') for header in headers: - if header != header_name: + if header != tag: + # Resetting the symbol of all other headers self.alarms_table.heading(header, text=header + ' ●') self.controller.update() - self.refresh_alarms_table() + self._refresh_alarms_table() + + def _flick_new(self) -> None: + """ + Defines actions once the new alarms only button is clicked + """ - def flick_new(self): + # Switching if the alarm is raised or sunken if self.controller.get_new(): self.alarms_button_new.config(relief='raised') else: self.alarms_button_new.config(relief='sunken') + + # Updating filters and table self.controller.toggle_new_only() self.controller.update() - self.refresh_alarms_table() + self._refresh_alarms_table() - def flick_priority(self, index: int): + def _flick_priority(self, index: int) -> None: + """ + Defines actions once one priority filter button is clicked + + :param index: Indicates which priority filter button was clicked + """ + + # Switching if the alarm is raised or sunken tag = self.dangers[index] if tag in self.controller.get_priorities(): self.alarms_buttons_priority[index].config(relief='raised') else: self.alarms_buttons_priority[index].config(relief='sunken') - self.controller.toggle_priority(tag) - self.refresh_alarms_table() - def flick_criticality(self, index: int): + # Updating filters and table + self.controller.toggle_priority(Tag(tag)) + self._refresh_alarms_table() + + def _flick_criticality(self, index: int) -> None: + """ + Defines actions once one criticality filter button is clicked + + :param index: Indicates which criticality filter button was clicked + """ + + # Switching if the alarm is raised or sunken tag = self.dangers[index] if tag in self.controller.get_criticalities(): self.alarms_buttons_criticality[index].config(relief='raised') else: self.alarms_buttons_criticality[index].config(relief='sunken') - self.controller.toggle_criticality(tag) - self.refresh_alarms_table() - def update_alarm_registered_times( + # Updating filters and table + self.controller.toggle_criticality(Tag(tag)) + self._refresh_alarms_table() + + def _update_alarm_registered_times( self, start_time: datetime | None, end_time: datetime | None ) -> OperationControl: + """ + Defines actions once the input registered time window has been updated + + :param start_time: The minimum time alarms should have been registered from + :param end_time: The maximum time alarms should have been registered by + :return: An operational code indicating what should occur next + """ + + # Requesting updates to shown alarms + self.controller.set_registered_start_time(start_time) self.controller.set_registered_end_time(end_time) self.controller.update() - self.refresh_alarms_table() + + self._refresh_alarms_table() return OperationControl.CONTINUE - def update_alarm_confirmed_times( + def _update_alarm_confirmed_times( self, start_time: datetime | None, end_time: datetime | None ) -> OperationControl: + """ + Defines actions once the input confirmed time window has been updated + + :param start_time: The minimum time alarms should have been confirmed from + :param end_time: The maximum time alarms should have been confirmed by + """ + + # Requesting updates to shown alarms self.controller.set_confirmed_start_time(start_time) self.controller.set_confirmed_end_time(end_time) self.controller.update() - self.refresh_alarms_table() - self.refresh_alarms_table() + + self._refresh_alarms_table() return OperationControl.CONTINUE - def flick_type(self, index: int): + def _flick_type(self, index: int) -> None: + """ + Defines actions once one alarm type filter button is clicked + + :param index: Indicates which alarm type filter button was clicked + """ + + # Switching if the alarm is raised or sunken tag = self.types[index] if tag in self.controller.get_types(): self.alarms_buttons_type[index].config(relief='raised') else: self.alarms_buttons_type[index].config(relief='sunken') - self.controller.toggle_type(tag) - self.refresh_alarms_table() - def double_click_alarms_table_row(self, event) -> None: + # Updating filters and table + self.controller.toggle_type(Tag(tag)) + self._refresh_alarms_table() + + def _double_click_alarms_table_row(self, event) -> None: """ This method specifies what happens if a double click were to happen in the alarms table @@ -220,14 +292,19 @@ def double_click_alarms_table_row(self, event) -> None: index = self.alarms_table.index(cur_item) alarm = self.controller.get_table_entries()[index][-1] - self.open_alarm_popup(alarm) + self._open_alarm_popup(alarm) + + def construct_alarms_table(self, event: Event = None) -> None: + """ + Makes a request to obtain all data pertaining to the current telemetry frame and + reconstructs the shown data table + """ - def construct_alarms_table(self, event: Event = None): self.controller.toggle_all() self.controller.create(self.dm) - self.refresh_alarms_table() + self._refresh_alarms_table() - def refresh_alarms_table(self) -> None: + def _refresh_alarms_table(self) -> None: """ This method wipes the data from the dashboard table and re-inserts the new values @@ -237,16 +314,18 @@ def refresh_alarms_table(self) -> None: for item in self.controller.get_table_entries(): self.alarms_table.insert('', END, values=tuple(item)) - def open_alarm_popup(self, alarm: Alarm) -> None: + def _open_alarm_popup(self, alarm: Alarm) -> None: """ This method opens a popup displaying details and options for an alarm - Args: - alarm (Alarm): the alarm the popup pertains to + :param: alarm (Alarm): the alarm the popup pertains to """ + new_window = Toplevel(self.overall_frame) new_window.grab_set() event = alarm.event + + # Configuring shown alarm values new_window.title(f'{'[NEW] ' if not alarm.acknowledged else ''}Alarm #{event.id}') new_window.geometry('300x300') Label(new_window, text=f'Priority: {alarm.priority.value}', anchor='w').pack(fill=BOTH) @@ -267,44 +346,57 @@ def open_alarm_popup(self, alarm: Alarm) -> None: Label(new_window).pack() Label(new_window, text=f'Description: {event.description}', anchor='w').pack(fill=BOTH) buttons = Frame(new_window) + + # If an alarm is not acknowledged, show an extra button to let it be acknowledged if not alarm.acknowledged: Button( buttons, text='Acknowledge', width=12, - command=lambda: self.acknowledge_alarm(alarm, new_window) + command=lambda: self._acknowledge_alarm(alarm, new_window) ).grid(row=0, column=0, padx=10, pady=10) + + # Configuring the remove alarm button Button( - buttons, text='Remove', width=12, command=lambda: self.remove_alarm(alarm, new_window) + buttons, text='Remove', width=12, command=lambda: self._remove_alarm(alarm, new_window) ).grid(row=0, column=1, padx=10, pady=10) buttons.pack(side=BOTTOM) - def acknowledge_alarm(self, alarm: Alarm, popup: Toplevel) -> None: + def _acknowledge_alarm(self, alarm: Alarm, popup: Toplevel) -> None: """ This method handles acknowledging an alarm. - Args: - alarm (Alarm): the alarm to acknowledge - popup (Toplevel): the popup that the alarm acknowledgement happens from, - closed upon alarm acknowledgement + :param: alarm: the alarm to acknowledge + :type: Alarm + + :param: popup: the popup that the alarm acknowledgement happens from, + closed upon alarm acknowledgement + :type: Toplevel """ + self.controller.acknowledge_alarm(alarm, self.dm) popup.destroy() - def remove_alarm(self, alarm: Alarm, popup: Toplevel) -> None: + def _remove_alarm(self, alarm: Alarm, popup: Toplevel) -> None: """ This method handles removing an alarm. - Args: - alarm (Alarm): the alarm to remove - popup (Toplevel): the popup that the alarm removal happens from, - closed upon alarm removal + :param: alarm (Alarm): the alarm to remove + :type: Alarm + + :param: popup: the popup that the alarm removal happens from, + closed upon alarm removal + :type: Toplevel """ if messagebox.askokcancel(title='Remove alarm', message=f'Remove alarm #{alarm.event.id}?'): self.controller.remove_alarm(alarm, self.dm) popup.destroy() def alarms_searcher_update(self): + """Watcher method that handles updates to the alarm tag searcher""" + # Convert to list to enforce ordering selected_tags = self.alarms_searcher.selected_tags self.controller.set_shown_tags(selected_tags) + + # Updating shown alarms self.controller.update() - self.refresh_alarms_table() + self._refresh_alarms_table() diff --git a/src/astra/usecase/alarms_request_receiver.py b/src/astra/usecase/alarms_request_receiver.py index b689c48..b5d88e1 100644 --- a/src/astra/usecase/alarms_request_receiver.py +++ b/src/astra/usecase/alarms_request_receiver.py @@ -393,8 +393,10 @@ def set_registered_start_time(cls, start_time: datetime): """ setter method for """ - - cls.filters.registered_start_time = start_time + if start_time is None: + cls.filters.registered_start_time = datetime.min + else: + cls.filters.registered_start_time = start_time @classmethod def set_registered_end_time(cls, end_time: datetime): @@ -402,23 +404,30 @@ def set_registered_end_time(cls, end_time: datetime): setter method for """ - cls.filters.registered_end_time = end_time + if end_time is None: + cls.filters.registered_end_time = datetime.max + else: + cls.filters.registered_end_time = end_time @classmethod def set_confirmed_start_time(cls, start_time: datetime): """ setter method for """ - - cls.filters.confirmed_start_time = start_time + if start_time is None: + cls.filters.confirmed_start_time = datetime.min + else: + cls.filters.confirmed_start_time = start_time @classmethod def set_confirmed_end_time(cls, end_time: datetime): """ setter method for """ - - cls.filters.confirmed_end_time = end_time + if end_time is None: + cls.filters.confirmed_end_time = datetime.max + else: + cls.filters.confirmed_end_time = end_time @classmethod def set_new_alarms(cls, new: bool): From 584a75e5dbd1dd1d83f813381ae8c957aa777dcb Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Thu, 7 Dec 2023 13:38:54 -0500 Subject: [PATCH 32/47] flake8 fix --- src/astra/usecase/alarm_strategies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/astra/usecase/alarm_strategies.py b/src/astra/usecase/alarm_strategies.py index d993b2d..3b20af9 100644 --- a/src/astra/usecase/alarm_strategies.py +++ b/src/astra/usecase/alarm_strategies.py @@ -16,7 +16,6 @@ next_id = EventID(0) - def get_strategy(base: EventBase) -> Callable: """ Matches an unknown form of EventBase to the correct strategy to check them @@ -612,6 +611,7 @@ def threshold_check(dm: DataManager, alarm_base: ThresholdEventBase, return all_alarm_frames return [] + def setpoint_cond(param_value: ParameterValue, setpoint: ParameterValue) -> bool: """ Checks if param_value is at it's associated setpoint exactly From 51c5f8b3774a6f30063ba91c6b195cf46c9f02ff Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Thu, 7 Dec 2023 13:43:42 -0500 Subject: [PATCH 33/47] completed documentation --- src/astra/frontend/graphing_view.py | 45 +++++++++++++++-------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/astra/frontend/graphing_view.py b/src/astra/frontend/graphing_view.py index c27e6fb..6d9e36c 100644 --- a/src/astra/frontend/graphing_view.py +++ b/src/astra/frontend/graphing_view.py @@ -20,14 +20,14 @@ class GraphingView: """ Defines the actual view and some minor logic for the graphing tab - :param dm: Contains all data known to the program - :param overall_frame: Contains all visual aspects of the view - :param controller: Interface for backend requests - :param searcher: The tag searcher used to select parameters to plot - :param figure: The actual graphing data to be shown - :param figure_canvas: A canvas to display on - :param y_axis_selection_text: Contains the most recently selected y-axis selection option - :param y_axis_selector: Allows user to select a tag to use for y-axis values + :param: dm: Contains all data known to the program + :param: overall_frame: Contains all visual aspects of the view + :param: controller: Interface for backend requests + :param: searcher: The tag searcher used to select parameters to plot + :param: figure: The actual graphing data to be shown + :param: figure_canvas: A canvas to display on + :param: y_axis_selection_text: Contains the most recently selected y-axis selection option + :param: y_axis_selector: Allows user to select a tag to use for y-axis values """ def __init__(self, frame: ttk.Notebook, num_rows: int, width, dm: DataManager): @@ -61,7 +61,7 @@ def __init__(self, frame: ttk.Notebook, num_rows: int, width, dm: DataManager): graphing_time_option = Frame(graphing_frame) graphing_time_option.grid(sticky='news', row=0, column=0) - time_input = TimerangeInput(graphing_time_option, 'Time range', self.times_update) + time_input = TimerangeInput(graphing_time_option, 'Time range', self._times_update) time_input.grid(row=0, column=0, padx=20, pady=20, ) # Creating graphing region UI @@ -84,15 +84,15 @@ def __init__(self, frame: ttk.Notebook, num_rows: int, width, dm: DataManager): self.y_axis_selector = Combobox(y_axis_selection_region, textvariable=self.y_axis_selection_text) self.y_axis_selector.grid(row=0, column=1, padx=5, pady=20) - self.y_axis_selector.bind('<>', self.set_graph_y_axis_label) + self.y_axis_selector.bind('<>', self._set_graph_y_axis_label) # Creating region for export data button button_selection_region = Frame(graphing_frame) button_selection_region.grid(sticky='nes', row=2, column=0, pady=20) - export_data_button = Button(button_selection_region, text='Export Data', - command=self.export_data) - export_data_button.grid(row=0, column=1, pady=20) + _export_data_button = Button(button_selection_region, text='Export Data', + command=self._export_data) + _export_data_button.grid(row=0, column=1, pady=20) self.searcher.deselect_all_tags() @@ -124,9 +124,9 @@ def searcher_update(self) -> None: self.y_axis_selection_text.set(detailed_tags[0]) self.controller.set_shown_tags(selected_tags) - self.create_graph() + self._create_graph() - def times_update(self, start_time: datetime | None, end_time: datetime | None) -> ( + def _times_update(self, start_time: datetime | None, end_time: datetime | None) -> ( OperationControl): """ Changes filtering for times once the user changes their input @@ -143,20 +143,23 @@ def times_update(self, start_time: datetime | None, end_time: datetime | None) - return OperationControl.CANCEL self.controller.set_start_date(start_time) self.controller.set_end_date(end_time) - self.create_graph() + self._create_graph() return OperationControl.CONTINUE - def export_data(self) -> None: + def _export_data(self) -> None: + """ + Defines actions once the export data button is clicked + """ config_path = filedialog.asksaveasfilename(title='Save file as', defaultextension='.csv', filetypes=[('csv file', '.csv')]) if config_path: try: - self.controller.export_data_to_file(config_path) + self.controller._export_data_to_file(config_path) except Exception as e: messagebox.showerror(title='Could not save data', message=f'{type(e).__name__}: ' f'{e}') - def create_graph(self) -> None: + def _create_graph(self) -> None: """ Constructs the graph according to previous user inputs """ @@ -219,12 +222,12 @@ def create_graph(self) -> None: new_plot.set_xticks(xticks_positions) new_plot.set_xticklabels(xticks_labels, rotation=0, fontsize=7) new_plot.legend() - self.set_graph_y_axis_label() + self._set_graph_y_axis_label() self.figure_canvas.draw() self.figure_canvas.get_tk_widget().pack() - def set_graph_y_axis_label(self, args: any = None) -> None: + def _set_graph_y_axis_label(self, args: any = None) -> None: """ Changes the y_axis range for the graph to the selected tag from the dropdown From 8ec818d7cb1f8c89d95a87e9281976ec2b321376 Mon Sep 17 00:00:00 2001 From: Liam Odero Date: Thu, 7 Dec 2023 13:46:09 -0500 Subject: [PATCH 34/47] flake8 fixes --- src/astra/frontend/graphing_view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/astra/frontend/graphing_view.py b/src/astra/frontend/graphing_view.py index 6d9e36c..177a9ee 100644 --- a/src/astra/frontend/graphing_view.py +++ b/src/astra/frontend/graphing_view.py @@ -90,9 +90,9 @@ def __init__(self, frame: ttk.Notebook, num_rows: int, width, dm: DataManager): button_selection_region = Frame(graphing_frame) button_selection_region.grid(sticky='nes', row=2, column=0, pady=20) - _export_data_button = Button(button_selection_region, text='Export Data', + export_data_button = Button(button_selection_region, text='Export Data', command=self._export_data) - _export_data_button.grid(row=0, column=1, pady=20) + export_data_button.grid(row=0, column=1, pady=20) self.searcher.deselect_all_tags() From a747c4095ea9e426bfa47a1417716334960521b6 Mon Sep 17 00:00:00 2001 From: shape-warrior-t Date: Thu, 7 Dec 2023 16:36:56 -0500 Subject: [PATCH 35/47] Fix fixable mypy complaints in timerange_input.py --- src/astra/frontend/timerange_input.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/astra/frontend/timerange_input.py b/src/astra/frontend/timerange_input.py index f3e7103..199ced9 100644 --- a/src/astra/frontend/timerange_input.py +++ b/src/astra/frontend/timerange_input.py @@ -105,7 +105,8 @@ def __init__(self, timerange_input: TimerangeInput): time_entries: list[tuple[Entry, StringVar, int]] = [] for row, time_vars in enumerate([self.start_time_vars, self.end_time_vars]): time_display_frame = Frame(self) - for element in [4, '-', 2, '-', 2, ' ', 2, ':', 2, ':', 2, ' ']: + elements: list[int | str] = [4, '-', 2, '-', 2, ' ', 2, ':', 2, ':', 2, ' '] + for element in elements: if isinstance(element, int): time_var = StringVar() time_vars.append(time_var) @@ -197,7 +198,7 @@ def set_entries_by_time(time_vars: list[StringVar], time: datetime | None) -> No def set_timerange(self) -> None: """Actually perform the action of setting a timerange.""" - times = [None, None] + times: list[datetime | None] = [None, None] for i, (time_vars, time_type_capitalized, time_type_lowercase) in enumerate( [(self.start_time_vars, 'Start', 'start'), (self.end_time_vars, 'End', 'end')] ): From e3334885fea7845f0fb4fe32dc2ccfed8293a52a Mon Sep 17 00:00:00 2001 From: shape-warrior-t Date: Thu, 7 Dec 2023 16:39:08 -0500 Subject: [PATCH 36/47] Add -> None return for consistency --- src/astra/frontend/timerange_input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/astra/frontend/timerange_input.py b/src/astra/frontend/timerange_input.py index 199ced9..b32025b 100644 --- a/src/astra/frontend/timerange_input.py +++ b/src/astra/frontend/timerange_input.py @@ -234,7 +234,7 @@ def set_timerange(self) -> None: self.timerange_input.update_timerange(start_time, end_time) @staticmethod - def switch_entry(entry: Entry, next_entry: Entry, entry_len: int): + def switch_entry(entry: Entry, next_entry: Entry, entry_len: int) -> None: """Switch focus from entry to next_entry as long as entry has entry_length characters.""" if len(entry.get()) == entry_len: next_entry.focus_set() From eceea1b8a2e7f7b8ccd070da3caea2d3e72c5fcc Mon Sep 17 00:00:00 2001 From: shape-warrior-t Date: Thu, 7 Dec 2023 17:08:01 -0500 Subject: [PATCH 37/47] Fix mypy errors --- src/astra/data/dict_parsing.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/astra/data/dict_parsing.py b/src/astra/data/dict_parsing.py index f171162..e182e08 100644 --- a/src/astra/data/dict_parsing.py +++ b/src/astra/data/dict_parsing.py @@ -24,7 +24,11 @@ AllEventBase, AnyEventBase, ) -from astra.data.parameters import DisplayUnit, Parameter, ParameterValue, Tag +from astra.data.parameters import DisplayUnit, Parameter, Tag + + +# Define ParameterValue here as a UnionType to work around some mypy thing +ParameterValue: UnionType = bool | int | float class ParsingError(Exception): @@ -59,12 +63,12 @@ def __str__(self): ) -def _format_type(t: type) -> str: +def _format_type(t: type | UnionType) -> str: # Stringify types in a nice way. return repr('None' if t is NoneType else t.__name__ if isinstance(t, type) else str(t)) -def _check_type[T](path: Path, value: object, expected_type: type[T]) -> T: +def _check_type(path: Path, value: object, expected_type: type | UnionType) -> Any: # Return the value unchanged if it is of the expected type and raise an error otherwise. if isinstance(value, expected_type): return value From 058988f27972cfe8c41db9c789b788fec875c61b Mon Sep 17 00:00:00 2001 From: shape-warrior-t Date: Thu, 7 Dec 2023 17:09:02 -0500 Subject: [PATCH 38/47] Reformat alarm_container.py --- src/astra/data/alarm_container.py | 33 ++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/astra/data/alarm_container.py b/src/astra/data/alarm_container.py index 5aa39db..e6d6204 100644 --- a/src/astra/data/alarm_container.py +++ b/src/astra/data/alarm_container.py @@ -17,6 +17,7 @@ class AlarmObserver: :param: mutex: Synchronization tool as many threads may notify watchers of updates :type: Lock """ + watchers: list[Callable] = [] _mutex = Lock() @@ -52,10 +53,15 @@ class AlarmsContainer: :param observer: An Observer to monitor the state of the container :type: AlarmObserver """ + observer = AlarmObserver() - alarms: dict[str, list[Alarm]] = {AlarmPriority.WARNING.name: [], AlarmPriority.LOW.name: [], - AlarmPriority.MEDIUM.name: [], AlarmPriority.HIGH.name: [], - AlarmPriority.CRITICAL.name: []} + alarms: dict[str, list[Alarm]] = { + AlarmPriority.WARNING.name: [], + AlarmPriority.LOW.name: [], + AlarmPriority.MEDIUM.name: [], + AlarmPriority.HIGH.name: [], + AlarmPriority.CRITICAL.name: [], + } new_alarms: Queue[Alarm] = Queue() mutex = Lock() @@ -70,8 +76,9 @@ def get_alarms(cls) -> dict[str, list[Alarm]]: return cls.alarms.copy() @classmethod - def add_alarms(cls, alarms: list[Alarm], - apm: Mapping[timedelta, Mapping[AlarmCriticality, AlarmPriority]]) -> None: + def add_alarms( + cls, alarms: list[Alarm], apm: Mapping[timedelta, Mapping[AlarmCriticality, AlarmPriority]] + ) -> None: """ Updates the alarms global variable after acquiring the lock for it @@ -136,13 +143,21 @@ def add_alarms(cls, alarms: list[Alarm], # the index well later use into it time_interval = len(times) - len(associated_times) + i - new_timer = Timer(associated_time.seconds, cls._update_priority, - args=[alarm_data, alarm_crit, timedelta(minutes=times[time_interval]), apm]) + new_timer = Timer( + associated_time.seconds, + cls._update_priority, + args=[alarm_data, alarm_crit, timedelta(minutes=times[time_interval]), apm], + ) new_timer.start() @classmethod - def _update_priority(cls, alarm: Alarm, alarm_crit: AlarmCriticality, time: timedelta, - apm: Mapping[timedelta, Mapping[AlarmCriticality, AlarmPriority]]) -> None: + def _update_priority( + cls, + alarm: Alarm, + alarm_crit: AlarmCriticality, + time: timedelta, + apm: Mapping[timedelta, Mapping[AlarmCriticality, AlarmPriority]], + ) -> None: """ Uses the alarm priority matrix with