File: preflight.py

package info (click to toggle)
python-openleadr-python 0.5.34%2Bdfsg.1-2
  • links: PTS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,496 kB
  • sloc: python: 6,942; xml: 663; makefile: 32; sh: 18
file content (149 lines) | stat: -rw-r--r-- 8,242 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# SPDX-License-Identifier: Apache-2.0

# Copyright 2020 Contributors to OpenLEADR

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from datetime import datetime, timedelta, timezone
from dataclasses import asdict, is_dataclass
from openleadr import enums, utils
import logging
logger = logging.getLogger('openleadr')


def preflight_message(message_type, message_payload):
    """
    Tests message contents before sending them. It will correct benign errors
    and warn you about them. Uncorrectable errors will raise an Exception. It
    changes the message_payload dict in-place.

    :param message_type string: The type of message you are sending
    :param message_payload dict: The contents of the message
    """
    if f'_preflight_{message_type}' in globals():
        message_payload = message_payload.copy()
        for key, value in message_payload.items():
            if isinstance(value, list):
                message_payload[key] = [asdict(item) if is_dataclass(item) else item
                                        for item in value]
            else:
                message_payload[key] = asdict(value) if is_dataclass(value) else value
        globals()[f'_preflight_{message_type}'](message_payload)
    return message_payload


def _preflight_oadrRegisterReport(message_payload):
    for report in message_payload['reports']:
        # Check that the report name is preceded by METADATA_ when registering reports
        if report['report_name'] in enums.REPORT_NAME.values \
                and not report['report_name'].startswith("METADATA"):
            report['report_name'] = 'METADATA_' + report['report_name']

        # Check that the measurement name and description match according to the schema
        for report_description in report['report_descriptions']:
            if 'measurement' in report_description and report_description['measurement'] is not None:
                utils.validate_report_measurement_dict(report_description['measurement'])

        # Add the correct namespace to the measurement
        for report_description in report['report_descriptions']:
            if 'measurement' in report_description and report_description['measurement'] is not None:
                if report_description['measurement']['name'] in enums._MEASUREMENT_NAMESPACES:
                    measurement_name = report_description['measurement']['name']
                    measurement_ns = enums._MEASUREMENT_NAMESPACES[measurement_name]
                    report_description['measurement']['ns'] = measurement_ns
                else:
                    raise ValueError("The Measurement Name is unknown")


def _preflight_oadrDistributeEvent(message_payload):
    if 'parse_duration' not in globals():
        from .utils import parse_duration
    # Check that the total event_duration matches the sum of the interval durations (rule 8)
    for event in message_payload['events']:
        active_period_duration = event['active_period']['duration']
        signal_durations = []
        for signal in event['event_signals']:
            signal_durations.append(sum([parse_duration(i['duration'])
                                         for i in signal['intervals']], timedelta(seconds=0)))

        if not all([d == active_period_duration for d in signal_durations]):
            if not all([d == signal_durations[0] for d in signal_durations]):
                raise ValueError("The different EventSignals have different total durations. "
                                 "Please correct this.")
            else:
                logger.warning(f"The active_period duration for event "
                               f"{event['event_descriptor']['event_id']} ({active_period_duration})"
                               f" differs from the sum of the interval's durations "
                               f"({signal_durations[0]}). The active_period duration has been "
                               f"adjusted to ({signal_durations[0]}).")
                event['active_period']['duration'] = signal_durations[0]

    # Check that payload values with signal name SIMPLE are constricted (rule 9)
    for event in message_payload['events']:
        for event_signal in event['event_signals']:
            if event_signal['signal_name'] == "SIMPLE":
                for interval in event_signal['intervals']:
                    if interval['signal_payload'] not in (0, 1, 2, 3):
                        raise ValueError("Payload Values used with Signal Name SIMPLE "
                                         "must be one of 0, 1, 2 or 3")

    # Check that the current_value is 0 for SIMPLE events that are not yet active (rule 14)
    for event in message_payload['events']:
        for event_signal in event['event_signals']:
            if 'current_value' in event_signal and event_signal['current_value'] != 0:
                if event_signal['signal_name'] == "SIMPLE" \
                        and event['event_descriptor']['event_status'] != "ACTIVE":
                    logger.warning("The current_value for a SIMPLE event "
                                   "that is not yet active must be 0. "
                                   "This will be corrected.")
                    event_signal['current_value'] = 0

    # Add the correct namespace to the measurement
    for event in message_payload['events']:
        for event_signal in event['event_signals']:
            if 'measurement' in event_signal and event_signal['measurement'] is not None:
                if event_signal['measurement']['name'] in enums._MEASUREMENT_NAMESPACES:
                    measurement_name = event_signal['measurement']['name']
                    measurement_ns = enums._MEASUREMENT_NAMESPACES[measurement_name]
                    event_signal['measurement']['ns'] = measurement_ns
                else:
                    raise ValueError("The Measurement Name is unknown")

    # Check that there is a valid oadrResponseRequired value for each Event
    for event in message_payload['events']:
        if 'response_required' not in event:
            event['response_required'] = 'always'
        elif event['response_required'] not in ('never', 'always'):
            logger.warning(f"The response_required property in an Event "
                           f"should be 'never' or 'always', not "
                           f"{event['response_required']}. Changing to 'always'.")
            event['response_required'] = 'always'

    # Check that there is a valid oadrResponseRequired value for each Event
    for event in message_payload['events']:
        if 'created_date_time' not in event['event_descriptor'] \
                or not event['event_descriptor']['created_date_time']:
            logger.warning("Your event descriptor did not contain a created_date_time. "
                           "This will be automatically added.")
            event['event_descriptor']['created_date_time'] = datetime.now(timezone.utc)

    # Check that the target designations are correct and consistent
    for event in message_payload['events']:
        if 'targets' in event and 'targets_by_type' in event:
            if utils.group_targets_by_type(event['targets']) != event['targets_by_type']:
                raise ValueError("You assigned both 'targets' and 'targets_by_type' in your event, "
                                 "but the two were not consistent with each other. "
                                 f"You supplied 'targets' = {event['targets']} and "
                                 f"'targets_by_type' = {event['targets_by_type']}")
        elif 'targets_by_type' in event and 'targets' not in event:
            event['targets'] = utils.ungroup_targets_by_type(event['targets_by_type'])