Passed
Pull Request — master (#135)
by Aldo
03:45
created

build.models.MaintenanceWindows.__getitem__()   A

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, timezone
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: Optional[datetime] = Field(default=datetime.max.replace(tzinfo=timezone.utc))
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 end_time is None:
77
            end_time = cls.model_fields["end"].get_default()
78 1
        if "start" in values.data and end_time <= values.data["start"]:
79 1
            raise ValueError("End before start not allowed")
80 1
        return end_time
81
82 1
    @model_validator(mode="after")
83 1
    def check_items_empty(self):
84
        """Check if no items are in the maintenance window."""
85 1
        no_items = all(
86
            map(
87
                lambda key: key not in self.model_dump()
88
                or len(self.model_dump()[key]) == 0,
89
                ["switches", "links", "interfaces"],
90
            )
91
        )
92 1
        if no_items:
93 1
            raise ValueError("At least one item must be provided")
94 1
        return self
95
96
    # pylint: enable=no-self-argument
97
98 1
    def __str__(self) -> str:
99
        return f"'{self.id}'<{self.start} to {self.end}>"
100
101 1
    class Config:
102
        """Config for encoding MaintenanceWindow class"""
103
104 1
        json_encoders = {
105
            datetime: lambda v: v.strftime(TIME_FMT),
106
        }
107
108
109 1
class MaintenanceWindows(RootModel):
110
    """List of Maintenance Windows for json conversion."""
111
112 1
    root: list[MaintenanceWindow]
113
114 1
    def __iter__(self):
115
        return iter(self.root)
116
117 1
    def __getitem__(self, item):
118
        return self.root[item]
119
120 1
    def __len__(self):
121
        return len(self.root)
122
123 1
    class Config:
124
        """Config for encoding MaintenanceWindows class"""
125
126 1
        json_encoders = {
127
            datetime: lambda v: v.strftime(TIME_FMT),
128
        }
129
130
131 1
class OverlapError(Exception):
132
    """
133
    Exception for when a Maintenance Windows execution
134
    period overlaps with one or more windows.
135
    """
136
137 1
    new_window: MaintenanceWindow
138 1
    interfering: MaintenanceWindows
139
140 1
    def __init__(self, new_window: MaintenanceWindow, interfering: MaintenanceWindows):
141
        self.new_window = new_window
142
        self.interfering = interfering
143
144 1
    def __str__(self):
145
        return (
146
            f"Maintenance Window {self.new_window} "
147
            + "interferes with the following windows: "
148
            + "["
149
            + ", ".join([f"{window}" for window in self.interfering])
150
            + "]"
151
        )
152