-
Notifications
You must be signed in to change notification settings - Fork 128
/
bulletin_models.py
245 lines (212 loc) · 7.95 KB
/
bulletin_models.py
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
import datetime
from abc import ABC
from typing import Set
from covid19br.common.city_name_helpers import fix_city_name
from covid19br.common.constants import NOT_INFORMED_CODE, PlaceType, State
from covid19br.common.data_normalization_utils import NormalizationUtils
from covid19br.common.exceptions import BadReportError
class BulletinModel(ABC):
"""
Represents a line of the csv generated in a status report.
It has the number of cases/deaths reported for a single city
or total in the state.
"""
date: datetime.date
sources: Set[str]
place_type: PlaceType
state: State
confirmed_cases: int
deaths: int
notes: str
def __init__(
self,
date,
source,
place_type,
state,
confirmed_cases=None,
deaths=None,
notes=None,
):
if not date:
raise BadReportError("date field is required in a report.")
if not source:
raise BadReportError("source field is required in a report.")
if not place_type:
raise BadReportError("place_type field is required in a report.")
self._check_state_or_raise_error(state)
self.state = state
self.set_confirmed_cases_value(confirmed_cases)
self.set_deaths_value(deaths)
self.date = date
self.sources = {source}
self.place_type = place_type
self.notes = notes
@staticmethod
def _check_state_or_raise_error(state):
if not state:
raise BadReportError("state field is required in a report.")
def set_confirmed_cases_value(self, confirmed_cases):
try:
cases_number = NormalizationUtils.ensure_integer(confirmed_cases)
self.confirmed_cases = (
cases_number if cases_number or cases_number == 0 else NOT_INFORMED_CODE
)
except ValueError:
raise BadReportError(
f"Invalid value for confirmed_cases: '{confirmed_cases}'. Value can't be cast to int."
)
def set_deaths_value(self, deaths):
try:
cases_number = NormalizationUtils.ensure_integer(deaths)
self.deaths = (
cases_number if cases_number or cases_number == 0 else NOT_INFORMED_CODE
)
except ValueError:
raise BadReportError(
f"Invalid value for deaths: '{deaths}'. Value can't be cast to int."
)
@property
def is_empty(self) -> bool:
return not self.has_deaths and not self.has_confirmed_cases
@property
def is_complete(self) -> bool:
return self.has_deaths and self.has_confirmed_cases
@property
def has_deaths(self) -> bool:
return self.deaths and self.deaths != NOT_INFORMED_CODE
@property
def has_confirmed_cases(self) -> bool:
return self.confirmed_cases and self.confirmed_cases != NOT_INFORMED_CODE
def to_csv_row(self):
raise NotImplementedError()
class StateTotalBulletinModel(BulletinModel):
def __init__(self, date, source, state, *args, **kwargs):
super().__init__(
date=date,
source=source,
place_type=PlaceType.STATE,
state=state,
*args,
**kwargs,
)
def __repr__(self):
return (
f"StateTotalBulletinModel("
f"date={self.date.strftime('%d/%m/%Y')}, "
f"state={self.state.value}, "
f"qtd_confirmed_cases={self.confirmed_cases}, "
f"qtd_deaths={self.deaths}"
f")"
)
def increase_deaths(self, value: int):
value = NormalizationUtils.ensure_integer(value)
if self.deaths == NOT_INFORMED_CODE:
self.deaths = value
return
self.deaths += value
def increase_confirmed_cases(self, value: int):
value = NormalizationUtils.ensure_integer(value)
if self.confirmed_cases == NOT_INFORMED_CODE:
self.confirmed_cases = value
return
self.confirmed_cases += value
def decrease_deaths(self, value: int):
value = NormalizationUtils.ensure_integer(value)
self.deaths -= value
if self.deaths < 0:
self.deaths = NOT_INFORMED_CODE
def decrease_confirmed_cases(self, value: int):
value = NormalizationUtils.ensure_integer(value)
self.confirmed_cases -= value
if self.confirmed_cases < 0:
self.confirmed_cases = NOT_INFORMED_CODE
def to_csv_row(self):
cases = self.confirmed_cases if self.confirmed_cases != NOT_INFORMED_CODE else 0
deaths = self.deaths if self.deaths != NOT_INFORMED_CODE else 0
return {"municipio": "TOTAL NO ESTADO", "confirmados": cases, "mortes": deaths}
class CountyBulletinModel(BulletinModel):
city: str
def __init__(self, date, source, state, city, *args, **kwargs):
if not city:
raise BadReportError("city field is required in a county report.")
self._check_state_or_raise_error(state)
self.city = fix_city_name(state.value, city)
super().__init__(
date=date,
source=source,
place_type=PlaceType.CITY,
state=state,
*args,
**kwargs,
)
def __repr__(self):
return (
f"CountyBulletinModel("
f"date={self.date.strftime('%d/%m/%Y')}, "
f"state={self.state.value}, "
f"city={self.city}, "
f"qtd_confirmed_cases={self.confirmed_cases}, "
f"qtd_deaths={self.deaths}"
f")"
)
def __eq__(self, other):
"""
Overrides the default implementation to better check if two
CountyBulletins have the same data (even if from different sources)
"""
if isinstance(other, self.__class__):
return (
self.state == other.state
and self.city == other.city
and self.date == other.date
and self.deaths == other.deaths
and self.confirmed_cases == other.confirmed_cases
)
else:
return False
def __hash__(self):
"""
Overrides the default implementation to faster lookup for
CountyBulletins for the same city in the same date.
"""
return hash((self.state, self.city, self.date))
def to_csv_row(self):
cases = self.confirmed_cases if self.confirmed_cases != NOT_INFORMED_CODE else 0
deaths = self.deaths if self.deaths != NOT_INFORMED_CODE else 0
return {"municipio": self.city, "confirmados": cases, "mortes": deaths}
def merge_data(self, other):
if isinstance(other, self.__class__) and hash(self) == hash(other):
if not self.has_deaths and other.has_deaths:
self.deaths = other.deaths
self.sources.update(other.sources)
if not self.has_confirmed_cases and other.has_confirmed_cases:
self.confirmed_cases = other.confirmed_cases
self.sources.update(other.sources)
class ImportedUndefinedBulletinModel(BulletinModel):
def __init__(self, date, source, state, *args, **kwargs):
super().__init__(
date=date,
source=source,
place_type=PlaceType.CITY,
state=state,
*args,
**kwargs,
)
def __repr__(self):
return (
f"ImportedUndefinedBulletinModel("
f"date={self.date.strftime('%d/%m/%Y')}, "
f"state={self.state.value}, "
f"qtd_confirmed_cases={self.confirmed_cases}, "
f"qtd_deaths={self.deaths}"
f")"
)
def to_csv_row(self):
cases = self.confirmed_cases if self.confirmed_cases != NOT_INFORMED_CODE else 0
deaths = self.deaths if self.deaths != NOT_INFORMED_CODE else 0
return {
"municipio": "Importados/Indefinidos",
"confirmados": cases,
"mortes": deaths,
}