-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathnonbillable_models.py
More file actions
168 lines (124 loc) · 5.16 KB
/
Copy pathnonbillable_models.py
File metadata and controls
168 lines (124 loc) · 5.16 KB
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import datetime
import pydantic
from typing import Annotated, TypeVar
from functools import lru_cache
from pathlib import Path
_MODELS_DIR = Path(__file__).parent
@lru_cache
def get_allowed_clusters() -> set[str]:
with open(_MODELS_DIR / "cluster_names.txt") as f:
return set(f.read().strip().split("\n"))
@lru_cache
def get_allowed_su_types() -> set[str]:
with open(_MODELS_DIR / "su_types.txt") as f:
return set(f.read().strip().split("\n"))
def validate_date(v: str) -> datetime.date:
return datetime.datetime.strptime(v, "%Y-%m").date()
DateField = Annotated[datetime.date, pydantic.BeforeValidator(validate_date)]
class NamedObject(pydantic.BaseModel):
name: str
T = TypeVar("T", bound=NamedObject)
class UniqueObjectList(pydantic.RootModel[list[T]]):
root: list[T]
@pydantic.model_validator(mode="after")
def validate_unique_names(self):
seen: set[str] = set()
for item in self.root:
if item.name in seen:
raise ValueError(f"{item.name}: found duplicate name")
seen.add(item.name)
return self
class ExcludedCluster(NamedObject):
start: DateField | None = None
end: DateField | None = None
reason: str | None = None
@pydantic.field_validator("name")
def only_allowed_cluster_names(cls, v):
allowed = get_allowed_clusters()
if v not in allowed:
raise ValueError(f"'{v}' is not a valid cluster name")
return v
ExcludedClusterList = UniqueObjectList[ExcludedCluster]
class ExcludedProject(NamedObject):
clusters: ExcludedClusterList = ExcludedClusterList([])
start: DateField | None = None
end: DateField | None = None
reason: str | None = None
is_billable: bool = False
@pydantic.model_validator(mode="after")
def validate_time_periods(self):
def is_date_range_valid(
start: datetime.date | None, end: datetime.date | None
) -> bool:
if start and end:
if end < start:
raise ValueError(
f"{self.name}: End date must be after start date for project"
)
elif start or end:
raise ValueError(
f"{self.name}: Start and end dates must be provided together or not at all"
)
return True
is_date_range_valid(self.start, self.end)
if self.clusters:
for excluded_cluster in self.clusters.root:
is_date_range_valid(excluded_cluster.start, excluded_cluster.end)
return self
class NonBilledSUType(NamedObject):
@pydantic.field_validator("name")
def only_allowed_su_types(cls, v):
allowed = get_allowed_su_types()
if v not in allowed:
raise ValueError(f"'{v}' is not a valid SU type")
return v
NonBilledSUTypeList = UniqueObjectList[NonBilledSUType]
class PIParticipant(pydantic.BaseModel):
name: str = pydantic.Field(alias="username")
non_billed_su_types: NonBilledSUTypeList | None = None
model_config = pydantic.ConfigDict(populate_by_name=True)
class PIList(UniqueObjectList[PIParticipant]):
def get_nonbillable_pis(self) -> list[str]:
return [pi.name for pi in self.root if pi.non_billed_su_types is None]
def get_pi_non_billed_su_types(self) -> dict[str, list[str]]:
return {
pi.name: [su.name for su in pi.non_billed_su_types.root]
for pi in self.root
if pi.non_billed_su_types is not None
}
class ExcludedProjectList(UniqueObjectList[ExcludedProject]):
def get_nonbillable_projects(
self, invoice_month: str
) -> list[tuple[str, str | None, bool, bool]]:
invoice_date = datetime.datetime.strptime(invoice_month, "%Y-%m").date()
def _is_in_time_range(start: datetime.date, end: datetime.date) -> bool:
return start <= invoice_date <= end
project_list = []
for project in self.root:
project_name = project.name
cluster_list = project.clusters.root
is_billable = project.is_billable
if project.start:
if not _is_in_time_range(project.start, project.end):
continue
if cluster_list:
for cluster in cluster_list:
project_list.append(
(project_name, cluster.name, True, is_billable)
)
else:
project_list.append((project_name, None, True, is_billable))
elif cluster_list:
for cluster in cluster_list:
if cluster.start:
if _is_in_time_range(cluster.start, cluster.end):
project_list.append(
(project_name, cluster.name, True, is_billable)
)
else:
project_list.append(
(project_name, cluster.name, False, is_billable)
)
else:
project_list.append((project_name, None, False, is_billable))
return project_list