Address and enable static type analysis checking of cara.
This commit is contained in:
parent
718a08ef1b
commit
64fa2b60a4
10 changed files with 110 additions and 73 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import html
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional, Awaitable
|
||||
from typing import Coroutine, Any, Optional, Awaitable
|
||||
|
||||
import jinja2
|
||||
import mistune
|
||||
|
|
@ -14,7 +14,7 @@ from .user import AuthenticatedUser, AnonymousUser
|
|||
|
||||
class BaseRequestHandler(RequestHandler):
|
||||
|
||||
async def prepare(self) -> Optional[Awaitable[None]]:
|
||||
async def prepare(self):
|
||||
"""Called at the beginning of a request before `get`/`post`/etc."""
|
||||
username = self.request.headers.get("X-ADFS-LOGIN", None)
|
||||
if username:
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ class FormData:
|
|||
def build_model(self) -> models.ExposureModel:
|
||||
return model_from_form(self)
|
||||
|
||||
def ventilation(self) -> models.Ventilation:
|
||||
def ventilation(self) -> typing.Union[models.Ventilation, models.MultipleVentilation]:
|
||||
always_on = models.PeriodicInterval(period=120, duration=120)
|
||||
# Initializes a ventilation instance as a window if 'natural' is selected, or as a HEPA-filter otherwise
|
||||
if self.ventilation_type == 'natural':
|
||||
|
|
@ -189,10 +189,12 @@ class FormData:
|
|||
inside_temp = models.PiecewiseConstant((0, 24), (293,))
|
||||
outside_temp = data.GenevaTemperatures[month]
|
||||
|
||||
ventilation: models.Ventilation
|
||||
if self.window_type == 'sliding':
|
||||
ventilation = models.SlidingWindow(
|
||||
active=window_interval,
|
||||
inside_temp=inside_temp, outside_temp=outside_temp,
|
||||
inside_temp=inside_temp,
|
||||
outside_temp=outside_temp,
|
||||
window_height=self.window_height,
|
||||
opening_length=self.opening_distance,
|
||||
number_of_windows=self.windows_number,
|
||||
|
|
@ -200,7 +202,8 @@ class FormData:
|
|||
elif self.window_type == 'hinged':
|
||||
ventilation = models.HingedWindow(
|
||||
active=window_interval,
|
||||
inside_temp=inside_temp, outside_temp=outside_temp,
|
||||
inside_temp=inside_temp,
|
||||
outside_temp=outside_temp,
|
||||
window_height=self.window_height,
|
||||
window_width=self.window_width,
|
||||
opening_length=self.opening_distance,
|
||||
|
|
@ -218,7 +221,7 @@ class FormData:
|
|||
|
||||
if self.hepa_option:
|
||||
hepa = models.HEPAFilter(active=always_on, q_air_mech=self.hepa_amount)
|
||||
return models.MultipleVentilation((ventilation,hepa))
|
||||
return models.MultipleVentilation((ventilation, hepa))
|
||||
else:
|
||||
return ventilation
|
||||
|
||||
|
|
@ -301,7 +304,7 @@ class FormData:
|
|||
)
|
||||
return exposed
|
||||
|
||||
def _compute_breaks_in_interval(self, start, finish, n_breaks) -> typing.Tuple[typing.Tuple[int, int]]:
|
||||
def _compute_breaks_in_interval(self, start, finish, n_breaks) -> models.BoundarySequence_t:
|
||||
break_delay = ((finish - start) - (n_breaks * self.coffee_duration)) // (n_breaks+1)
|
||||
break_times = []
|
||||
end = start
|
||||
|
|
@ -311,13 +314,13 @@ class FormData:
|
|||
break_times.append((begin, end))
|
||||
return tuple(break_times)
|
||||
|
||||
def lunch_break_times(self) -> typing.Tuple[typing.Tuple[int, int]]:
|
||||
def lunch_break_times(self) -> models.BoundarySequence_t:
|
||||
result = []
|
||||
if self.lunch_option:
|
||||
result.append((self.lunch_start, self.lunch_finish))
|
||||
return tuple(result)
|
||||
|
||||
def coffee_break_times(self) -> typing.Tuple[typing.Tuple[int, int]]:
|
||||
def coffee_break_times(self) -> models.BoundarySequence_t:
|
||||
if not self.coffee_breaks:
|
||||
return ()
|
||||
if self.lunch_option:
|
||||
|
|
@ -475,10 +478,10 @@ def expiration_blend(expiration_weights: typing.Dict[models.Expiration, int]) ->
|
|||
ejection_factor += np.array(expiration.ejection_factor) * weight
|
||||
particle_sizes += np.array(expiration.particle_sizes) * weight
|
||||
|
||||
return models.Expiration(
|
||||
ejection_factor=tuple(ejection_factor/total_weight),
|
||||
particle_sizes=tuple(particle_sizes/total_weight),
|
||||
)
|
||||
r_ejection_factor: typing.Tuple[float, float, float, float] = tuple(ejection_factor/total_weight) # type: ignore
|
||||
r_particle_sizes: typing.Tuple[float, float, float, float] = tuple(particle_sizes/total_weight) # type: ignore
|
||||
|
||||
return models.Expiration(ejection_factor=r_ejection_factor, particle_sizes=r_particle_sizes)
|
||||
|
||||
|
||||
def model_from_form(form: FormData) -> models.ExposureModel:
|
||||
|
|
|
|||
|
|
@ -113,9 +113,9 @@ def minutes_to_time(minutes: int) -> str:
|
|||
|
||||
return f"{hour_string}:{minute_string}"
|
||||
|
||||
def readable_minutes(minutes: int) -> str:
|
||||
|
||||
time = minutes
|
||||
def readable_minutes(minutes: int) -> str:
|
||||
time = float(minutes)
|
||||
unit = " minute"
|
||||
if time % 60 == 0:
|
||||
time = minutes/60
|
||||
|
|
@ -124,19 +124,20 @@ def readable_minutes(minutes: int) -> str:
|
|||
unit += "s"
|
||||
|
||||
if time.is_integer():
|
||||
time = "{:0.0f}".format(time)
|
||||
time_str = "{:0.0f}".format(time)
|
||||
else:
|
||||
time = "{0:.2f}".format(time)
|
||||
time_str = "{0:.2f}".format(time)
|
||||
|
||||
return time + unit
|
||||
return time_str + unit
|
||||
|
||||
def non_zero_percentage (percentage: int) -> str:
|
||||
|
||||
def non_zero_percentage(percentage: int) -> str:
|
||||
if percentage < 0.01:
|
||||
return "<0.01%"
|
||||
else:
|
||||
return "{:0.0f}%".format(percentage)
|
||||
|
||||
|
||||
def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, models.ExposureModel]:
|
||||
scenarios = {}
|
||||
|
||||
|
|
@ -243,7 +244,7 @@ def build_report(model: models.ExposureModel, form: FormData):
|
|||
cara_templates = Path(__file__).parent.parent / "templates"
|
||||
calculator_templates = Path(__file__).parent / "templates"
|
||||
env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader([cara_templates, calculator_templates]),
|
||||
loader=jinja2.FileSystemLoader([str(cara_templates), str(calculator_templates)]),
|
||||
undefined=jinja2.StrictUndefined,
|
||||
)
|
||||
env.filters['non_zero_percentage'] = non_zero_percentage
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ WidgetPairType = typing.Tuple[widgets.Widget, widgets.Widget]
|
|||
|
||||
|
||||
class WidgetGroup:
|
||||
def __init__(self, label_widget_pairs: typing.Sequence[WidgetPairType]):
|
||||
self.labels = []
|
||||
self.widgets = []
|
||||
def __init__(self, label_widget_pairs: typing.Iterable[WidgetPairType]):
|
||||
self.labels: typing.List[widgets.Widget] = []
|
||||
self.widgets: typing.List[widgets.Widget] = []
|
||||
self.add_pairs(label_widget_pairs)
|
||||
|
||||
def set_visible(self, visible: bool):
|
||||
|
|
@ -46,10 +46,10 @@ class WidgetGroup:
|
|||
widget.layout.visible = False
|
||||
widget.layout.display = 'none'
|
||||
|
||||
def pairs(self) -> typing.Sequence[WidgetPairType]:
|
||||
def pairs(self) -> typing.Iterable[WidgetPairType]:
|
||||
return zip(*[self.labels, self.widgets])
|
||||
|
||||
def add_pairs(self, label_widget_pairs: typing.Sequence[WidgetPairType]):
|
||||
def add_pairs(self, label_widget_pairs: typing.Iterable[WidgetPairType]):
|
||||
labels, widgets_ = zip(*label_widget_pairs)
|
||||
self.labels.extend(labels)
|
||||
self.widgets.extend(widgets_)
|
||||
|
|
@ -190,13 +190,13 @@ class ExposureComparissonResult(View):
|
|||
return ax
|
||||
|
||||
def scenarios_updated(self, scenarios: typing.Sequence[ScenarioType], _):
|
||||
labels, models = zip(*scenarios)
|
||||
conc_models: typing.Tuple[models.ConcentrationModel] = tuple(
|
||||
model.concentration_model.dcs_instance() for model in models
|
||||
updated_labels, updated_models = zip(*scenarios)
|
||||
conc_models: typing.Tuple[models.ConcentrationModel, ...] = tuple(
|
||||
model.concentration_model.dcs_instance() for model in updated_models
|
||||
)
|
||||
self.update_plot(conc_models, labels)
|
||||
self.update_plot(conc_models, updated_labels)
|
||||
|
||||
def update_plot(self, conc_models: typing.Tuple[models.ConcentrationModel], labels: typing.Tuple[str]):
|
||||
def update_plot(self, conc_models: typing.Tuple[models.ConcentrationModel, ...], labels: typing.Tuple[str, ...]):
|
||||
self.ax.lines.clear()
|
||||
start, finish = models_start_end(conc_models)
|
||||
ts = np.linspace(start, finish, num=250)
|
||||
|
|
@ -268,12 +268,12 @@ class ModelWidgets(View):
|
|||
|
||||
outside_temp.observe(outsidetemp_change, names=['value'])
|
||||
auto_width = widgets.Layout(width='auto')
|
||||
return WidgetGroup([
|
||||
[
|
||||
return WidgetGroup((
|
||||
(
|
||||
widgets.Label('Outside temperature (℃)', layout=auto_width,),
|
||||
outside_temp,
|
||||
],
|
||||
])
|
||||
),
|
||||
))
|
||||
|
||||
def _build_window(self, node) -> WidgetGroup:
|
||||
period = widgets.IntSlider(value=node.active.period, min=0, max=240)
|
||||
|
|
@ -314,24 +314,24 @@ class ModelWidgets(View):
|
|||
toggle_outsidetemp(outsidetemp_w.value)
|
||||
|
||||
auto_width = widgets.Layout(width='auto')
|
||||
result = WidgetGroup([
|
||||
[
|
||||
result = WidgetGroup((
|
||||
(
|
||||
widgets.Label('Interval between openings (minutes)', layout=auto_width),
|
||||
period,
|
||||
],
|
||||
[
|
||||
),
|
||||
(
|
||||
widgets.Label('Duration of opening (minutes)', layout=auto_width),
|
||||
interval,
|
||||
],
|
||||
[
|
||||
),
|
||||
(
|
||||
widgets.Label('Inside temperature (℃)', layout=auto_width),
|
||||
inside_temp,
|
||||
],
|
||||
[
|
||||
),
|
||||
(
|
||||
widgets.Label('Outside temperature scheme', layout=auto_width),
|
||||
outsidetemp_w,
|
||||
]
|
||||
])
|
||||
)
|
||||
))
|
||||
for sub_group in outsidetemp_widgets.values():
|
||||
result.add_pairs(sub_group.pairs())
|
||||
return result
|
||||
|
|
@ -362,9 +362,9 @@ class ModelWidgets(View):
|
|||
month_choice.observe(on_month_change, names=['value'])
|
||||
|
||||
return WidgetGroup(
|
||||
[
|
||||
[widgets.Label("Month"), month_choice],
|
||||
]
|
||||
(
|
||||
(widgets.Label("Month"), month_choice),
|
||||
)
|
||||
)
|
||||
|
||||
def _build_activity(self, node):
|
||||
|
|
|
|||
|
|
@ -2,13 +2,9 @@ import dataclasses
|
|||
import typing
|
||||
|
||||
|
||||
DCInst = typing.TypeVar('T')
|
||||
|
||||
|
||||
def nested_replace(obj: DCInst, new_values: typing.Dict[str, typing.Any]) -> DCInst:
|
||||
"""
|
||||
Replace an attribute on a dataclass, much like dataclasses.replace, except it
|
||||
supports nested replacement definitions. For example:
|
||||
def nested_replace(obj, new_values: typing.Dict[str, typing.Any]):
|
||||
"""Replace an attribute on a dataclass, much like dataclasses.replace,
|
||||
except it supports nested replacement definitions. For example:
|
||||
|
||||
>>> new_obj = nested_replace(obj, {'attr1.sub_attr2.sub_sub_attr3': 4})
|
||||
>>> new_obj.attr1.sub_attr2.sub_sub_attr3
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ class Room:
|
|||
volume: float
|
||||
|
||||
|
||||
Time_t = typing.TypeVar('Time_t', float, int)
|
||||
BoundaryPair_t = typing.Tuple[Time_t, Time_t]
|
||||
BoundarySequence_t = typing.Union[typing.Tuple[BoundaryPair_t, ...], typing.Tuple]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Interval:
|
||||
"""
|
||||
|
|
@ -26,7 +31,7 @@ class Interval:
|
|||
start < t <= end
|
||||
|
||||
"""
|
||||
def boundaries(self) -> typing.Tuple[typing.Tuple[float, float], ...]:
|
||||
def boundaries(self) -> BoundarySequence_t:
|
||||
return ()
|
||||
|
||||
def transition_times(self) -> typing.Set[float]:
|
||||
|
|
@ -48,23 +53,23 @@ class SpecificInterval(Interval):
|
|||
#: A sequence of times (start, stop), in hours, that the infected person
|
||||
#: is present. The flattened list of times must be strictly monotonically
|
||||
#: increasing.
|
||||
present_times: typing.Tuple[typing.Tuple[float, float], ...]
|
||||
present_times: BoundarySequence_t
|
||||
|
||||
def boundaries(self):
|
||||
def boundaries(self) -> BoundarySequence_t:
|
||||
return self.present_times
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PeriodicInterval(Interval):
|
||||
#: How often does the interval occur (minutes).
|
||||
period: int
|
||||
period: float
|
||||
|
||||
#: How long does the interval occur for (minutes).
|
||||
#: A value greater than :data:`period` signifies the event is permanently
|
||||
#: occurring, a value of 0 signifies that the event never happens.
|
||||
duration: int
|
||||
duration: float
|
||||
|
||||
def boundaries(self) -> typing.Tuple[typing.Tuple[float, float], ...]:
|
||||
def boundaries(self) -> BoundarySequence_t:
|
||||
if self.period == 0 or self.duration == 0:
|
||||
return tuple()
|
||||
result = []
|
||||
|
|
@ -91,16 +96,17 @@ class PiecewiseConstant:
|
|||
if tuple(sorted(set(self.transition_times))) != self.transition_times:
|
||||
raise ValueError("transition_times should not contain duplicated elements and should be sorted")
|
||||
|
||||
def value(self,time) -> float:
|
||||
def value(self, time) -> float:
|
||||
if time <= self.transition_times[0]:
|
||||
return self.values[0]
|
||||
if time > self.transition_times[-1]:
|
||||
elif time > self.transition_times[-1]:
|
||||
return self.values[-1]
|
||||
|
||||
for t1,t2,value in zip(self.transition_times[:-1],
|
||||
self.transition_times[1:],self.values):
|
||||
if time > t1 and time <= t2:
|
||||
return value
|
||||
for t1, t2, value in zip(self.transition_times[:-1],
|
||||
self.transition_times[1:], self.values):
|
||||
if t1 < time <= t2:
|
||||
break
|
||||
return value
|
||||
|
||||
def interval(self) -> Interval:
|
||||
# build an Interval object
|
||||
|
|
@ -109,7 +115,7 @@ class PiecewiseConstant:
|
|||
self.transition_times[1:],self.values):
|
||||
if value:
|
||||
present_times.append((t1,t2))
|
||||
return SpecificInterval(present_times=present_times)
|
||||
return SpecificInterval(present_times=tuple(present_times))
|
||||
|
||||
def refine(self,refine_factor=10):
|
||||
# build a new PiecewiseConstant object with a refined mesh,
|
||||
|
|
@ -258,10 +264,10 @@ class HingedWindow(WindowOpening):
|
|||
horizontal plane).
|
||||
"""
|
||||
#: Window width (m).
|
||||
window_width: float = None
|
||||
window_width: float = 0.0
|
||||
|
||||
def __post_init__(self):
|
||||
if self.window_width is None:
|
||||
if not self.window_width > 0:
|
||||
raise ValueError('window_width must be set')
|
||||
|
||||
@property
|
||||
|
|
@ -387,7 +393,10 @@ class Mask:
|
|||
#: Filtration efficiency of masks when inhaling.
|
||||
η_inhale: float
|
||||
|
||||
particle_sizes: typing.Tuple[float] = (0.8e-4, 1.8e-4, 3.5e-4, 5.5e-4) # In cm.
|
||||
#: Particle sizes in cm.
|
||||
particle_sizes: typing.Tuple[float, float, float, float] = (
|
||||
0.8e-4, 1.8e-4, 3.5e-4, 5.5e-4
|
||||
)
|
||||
|
||||
#: Pre-populated examples of Masks.
|
||||
types: typing.ClassVar[typing.Dict[str, "Mask"]]
|
||||
|
|
@ -542,7 +551,7 @@ class InfectedPopulation(Population):
|
|||
@dataclass(frozen=True)
|
||||
class ConcentrationModel:
|
||||
room: Room
|
||||
ventilation: Ventilation
|
||||
ventilation: typing.Union[Ventilation, MultipleVentilation]
|
||||
infected: InfectedPopulation
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ class DataclassStatePredefined(DataclassInstanceState):
|
|||
"""
|
||||
def __init__(self,
|
||||
dataclass: Datamodel_T,
|
||||
choices: typing.Dict[typing.Hashable, dataclass_instance],
|
||||
choices: typing.Dict[str, dataclass_instance],
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(dataclass=dataclass, **kwargs)
|
||||
|
|
|
|||
|
|
@ -19,3 +19,17 @@ def test_generate_report(baseline_form):
|
|||
|
||||
report = report_generator.build_report(model, baseline_form)
|
||||
assert report != ""
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["test_input", "expected"],
|
||||
[
|
||||
[1, '1 minute'],
|
||||
[2, '2 minutes'],
|
||||
[60, '1 hour'],
|
||||
[120, '2 hours'],
|
||||
[150, '150 minutes'],
|
||||
],
|
||||
)
|
||||
def test_readable_minutes(test_input, expected):
|
||||
assert report_generator.readable_minutes(test_input) == expected
|
||||
|
|
@ -25,7 +25,7 @@ class DCSimpleSubclass(DCSimple):
|
|||
|
||||
@dataclass
|
||||
class DCOverrideSubclass(DCSimple):
|
||||
attr1: float
|
||||
attr1: float # type: ignore
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
14
setup.cfg
Normal file
14
setup.cfg
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[mypy]
|
||||
no_warn_no_return = True
|
||||
|
||||
[mypy-matplotlib.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ipympl.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ipywidgets.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-mistune.*]
|
||||
ignore_missing_imports = True
|
||||
Loading…
Reference in a new issue