build.models.MaintenanceWindows.__getitem__()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.125

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 2
dl 0
loc 2
ccs 1
cts 2
cp 0.5
crap 1.125
rs 10
c 0
b 0
f 0
1
"""Models used by the maintenance NApp.
2
3
This module define models for the maintenance window itself and the
4
scheduler.
5
"""
6
7 1
from datetime import datetime
8 1
from enum import Enum
9 1
from typing import NewType, Optional
10 1
from uuid import uuid4
11
12 1
import pytz
13
14
# pylint: disable=no-name-in-module
15 1
from pydantic import (
16
    BaseModel,
17
    Field,
18
    RootModel,
19
    ValidationInfo,
20
    field_validator,
21
    model_validator,
22
)
23
24
# pylint: enable=no-name-in-module
25
26 1
TIME_FMT = "%Y-%m-%dT%H:%M:%S%z"
27
28
29 1
class Status(str, Enum):
30
    """Maintenance windows status."""
31
32 1
    PENDING = "pending"
33 1
    RUNNING = "running"
34 1
    FINISHED = "finished"
35
36
37 1
MaintenanceID = NewType("MaintenanceID", str)
38
39
40 1
class MaintenanceWindow(BaseModel):
41
    """Class for structure of maintenance windows."""
42
43 1
    start: datetime
44 1
    end: datetime
45 1
    switches: list[str] = Field(default_factory=list)
46 1
    interfaces: list[str] = Field(default_factory=list)
47 1
    links: list[str] = Field(default_factory=list)
48 1
    id: MaintenanceID = Field(default_factory=lambda: MaintenanceID(uuid4().hex))
49 1
    description: str = Field(default="")
50 1
    status: Status = Field(default=Status.PENDING)
51 1
    inserted_at: Optional[datetime] = Field(default=None)
52 1
    updated_at: Optional[datetime] = Field(default=None)
53
54
    # pylint: disable=no-self-argument
55
56 1
    @field_validator("start", "end", mode="before")
57 1
    @classmethod
58 1
    def convert_time(cls, time):
59
        """Convert time strings using TIME_FMT"""
60 1
        if isinstance(time, str):
61 1
            time = datetime.strptime(time, TIME_FMT)
62 1
        return time
63
64 1
    @field_validator("start")
65 1
    @classmethod
66 1
    def check_start_in_past(cls, start_time):
67
        """Check if the start is set to occur before now."""
68 1
        if start_time < datetime.now(pytz.utc):
69 1
            raise ValueError("Start in the past not allowed")
70 1
        return start_time
71
72 1
    @field_validator("end")
73 1
    @classmethod
74 1
    def check_end_before_start(cls, end_time, values: ValidationInfo):
75
        """Check if the end is set to occur before the start."""
76 1
        if "start" in values.data and end_time <= values.data["start"]:
77 1
            raise ValueError("End before start not allowed")
78 1
        return end_time
79
80 1
    @model_validator(mode="after")
81 1
    def check_items_empty(self):
82
        """Check if no items are in the maintenance window."""
83 1
        no_items = all(
84
            map(
85
                lambda key: key not in self.model_dump()
86
                or len(self.model_dump()[key]) == 0,
87
                ["switches", "links", "interfaces"],
88
            )
89
        )
90 1
        if no_items:
91 1
            raise ValueError("At least one item must be provided")
92 1
        return self
93
94
    # pylint: enable=no-self-argument
95
96 1
    def __str__(self) -> str:
97
        return f"'{self.id}'<{self.start} to {self.end}>"
98
99 1
    class Config:
100
        """Config for encoding MaintenanceWindow class"""
101
102 1
        json_encoders = {
103
            datetime: lambda v: v.strftime(TIME_FMT),
104
        }
105
106
107 1
class MaintenanceWindows(RootModel):
108
    """List of Maintenance Windows for json conversion."""
109
110 1
    root: list[MaintenanceWindow]
111
112 1
    def __iter__(self):
113
        return iter(self.root)
114
115 1
    def __getitem__(self, item):
116
        return self.root[item]
117
118 1
    def __len__(self):
119
        return len(self.root)
120
121 1
    class Config:
122
        """Config for encoding MaintenanceWindows class"""
123
124 1
        json_encoders = {
125
            datetime: lambda v: v.strftime(TIME_FMT),
126
        }
127
128
129 1
class OverlapError(Exception):
130
    """
131
    Exception for when a Maintenance Windows execution
132
    period overlaps with one or more windows.
133
    """
134
135 1
    new_window: MaintenanceWindow
136 1
    interfering: MaintenanceWindows
137
138 1
    def __init__(self, new_window: MaintenanceWindow, interfering: MaintenanceWindows):
139
        self.new_window = new_window
140
        self.interfering = interfering
141
142 1
    def __str__(self):
143
        return (
144
            f"Maintenance Window {self.new_window} "
145
            + "interferes with the following windows: "
146
            + "["
147
            + ", ".join([f"{window}" for window in self.interfering])
148
            + "]"
149
        )
150