Automatic report regeneration
This commit is contained in:
parent
1ff9a4d352
commit
2997607e60
11 changed files with 186 additions and 127 deletions
|
|
@ -33,7 +33,7 @@ A room number is included, if you do not wish to use a formal room number any re
|
|||
|
||||
### Room Data
|
||||
|
||||
Please enter either the room volume (in m<sup>3</sup>) or both the floor area (m<sup>2</sup>) and the room height (m).
|
||||
Please enter either the room volume (in m³) or both the floor area (m²) and the room height (m).
|
||||
This information is available via GIS Portal (https://gis.cern.ch/gisportal/).
|
||||
|
||||
### Ventilation type
|
||||
|
|
@ -99,7 +99,6 @@ As an example, for a shared office with 4 people, where one person is infected,
|
|||
There are a few predefined activities in the tool at present.
|
||||
|
||||
**Office ** = All persons seated, talking occasionally (1/3rd of the time, with normal breathing the other 2/3rds of the time). Everyone (exposed and infected occupants) is treated the same in this model.
|
||||
|
||||
**Meeting** = All persons seated, having a conversation (approximately each occupant is 1/N % of the time talking, where N is the number of occupants). Everyone (exposed and infected occupants) is treated the same in this model.
|
||||
|
||||
**Library** = All persons seated, breathing only (not talking), all the time.
|
||||
|
|
@ -191,9 +190,8 @@ It contains a summary of all the input data, which will allow the simulation to
|
|||
|
||||
This part of the report shows the ``P(i)`` or probability of one exposed person getting infected.
|
||||
It is estimated based on the emission rate of virus into the simulated volume, and the amount which is inhaled by exposed individuals.
|
||||
This probability is valid for the simulation duration - i.e. if you have simulated one day and plan to work 5 days in these conditions and the infected person emits the same amount of virus each day, the cumulative probability of infection is ``(1-(1-P(i))^5)```.
|
||||
This probability is valid for the simulation duration - i.e. if you have simulated one day and plan to work 5 days in these conditions and the infected person emits the same amount of virus each day, the cumulative probability of infection is ``(1-(1-P(i))^5)``.
|
||||
If you are using the natural ventilation option, the simulation is only valid for the selected month, because the following or preceding month will have a different average temperature profile.
|
||||
|
||||
The ``expected number of new cases`` for the simulation is calculated based on the probability of infection, multiplied by the number of exposed occupants.
|
||||
|
||||
### Exposure graph
|
||||
|
|
@ -207,6 +205,13 @@ It is determined by:
|
|||
* Under natural ventilation conditions, the effectiveness of ventilation relies upon the hourly temperature difference between the inside and outside air temperature.
|
||||
* A HEPA filter removes infectious quanta from the air at a constant rate and is modelled in the same way as mechanical ventilation, however air passed through a HEPA filter is recycled (i.e. it is not fresh air).
|
||||
|
||||
### QR code
|
||||
|
||||
At the end of the report you can find a unique QR code / hyperlink for this report. This provides an automatic way to review the calculator form with the corresponding specified parameters.
|
||||
This allows for:
|
||||
* sharing reports by either scanning or clicking on the QR code to obtain a shareable link.
|
||||
* easily regenerating reports with any new versions of the CARA model released in the future.
|
||||
|
||||
# Conclusion
|
||||
|
||||
This tool provides informative comparisons for COVID-19 (long-range) airborne risk only - see Disclaimer
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ class ConcentrationModel(BaseRequestHandler):
|
|||
self.finish(json.dumps(response_json))
|
||||
return
|
||||
|
||||
report = build_report(form.build_model(), form)
|
||||
base_url = self.request.protocol + "://" + self.request.host
|
||||
report = build_report(base_url, form.build_model(), form)
|
||||
self.finish(report)
|
||||
|
||||
|
||||
|
|
@ -63,7 +64,8 @@ class StaticModel(BaseRequestHandler):
|
|||
def get(self):
|
||||
form = model_generator.FormData.from_dict(model_generator.baseline_raw_form_data())
|
||||
model = form.build_model()
|
||||
report = build_report(model, form)
|
||||
base_url = self.request.protocol + "://" + self.request.host
|
||||
report = build_report(base_url, model, form)
|
||||
self.finish(report)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -82,66 +82,29 @@ class FormData:
|
|||
if value == "":
|
||||
form_data[key] = "0"
|
||||
|
||||
time_attributes = [
|
||||
'exposed_lunch_start', 'exposed_lunch_finish', 'exposed_start', 'exposed_finish',
|
||||
'infected_lunch_start', 'infected_lunch_finish', 'infected_start', 'infected_finish',
|
||||
]
|
||||
for attr_name in time_attributes:
|
||||
for attr_name in BOOLEAN_ATTRIBUTES:
|
||||
form_data[attr_name] = form_data[attr_name] == '1'
|
||||
for attr_name in FLOAT_ATTRIBUTES:
|
||||
form_data[attr_name] = float(form_data[attr_name])
|
||||
for attr_name in INT_ATTRIBUTES:
|
||||
form_data[attr_name] = int(form_data[attr_name])
|
||||
for attr_name in TIME_ATTRIBUTES:
|
||||
form_data[attr_name] = time_string_to_minutes(form_data[attr_name])
|
||||
|
||||
boolean_attributes = [
|
||||
'hepa_option', 'exposed_lunch_option', 'infected_lunch_option', 'infected_dont_have_breaks_with_exposed',
|
||||
]
|
||||
for attr_name in boolean_attributes:
|
||||
form_data[attr_name] = form_data[attr_name] == '1'
|
||||
|
||||
instance = cls(
|
||||
activity_type=form_data['activity_type'],
|
||||
air_changes=float(form_data['air_changes']),
|
||||
air_supply=float(form_data['air_supply']),
|
||||
ceiling_height=float(form_data['ceiling_height']),
|
||||
exposed_coffee_break_option=form_data['exposed_coffee_break_option'],
|
||||
exposed_coffee_duration=int(form_data['exposed_coffee_duration']),
|
||||
exposed_finish=form_data['exposed_finish'],
|
||||
exposed_lunch_finish=form_data['exposed_lunch_finish'],
|
||||
exposed_lunch_option=form_data['exposed_lunch_option'],
|
||||
exposed_lunch_start=form_data['exposed_lunch_start'],
|
||||
exposed_start=form_data['exposed_start'],
|
||||
floor_area=float(form_data['floor_area']),
|
||||
hepa_amount=float(form_data['hepa_amount']),
|
||||
hepa_option=form_data['hepa_option'],
|
||||
infected_coffee_break_option=form_data['infected_coffee_break_option'],
|
||||
infected_coffee_duration=int(form_data['infected_coffee_duration']),
|
||||
infected_dont_have_breaks_with_exposed=form_data['infected_dont_have_breaks_with_exposed'],
|
||||
infected_finish=form_data['infected_finish'],
|
||||
infected_lunch_finish=form_data['infected_lunch_finish'],
|
||||
infected_lunch_option=form_data['infected_lunch_option'],
|
||||
infected_lunch_start=form_data['infected_lunch_start'],
|
||||
infected_people=int(form_data['infected_people']),
|
||||
infected_start=form_data['infected_start'],
|
||||
mask_type=form_data['mask_type'],
|
||||
mask_wearing_option=form_data['mask_wearing_option'],
|
||||
mechanical_ventilation_type=form_data['mechanical_ventilation_type'],
|
||||
model_version=form_data['model_version'],
|
||||
opening_distance=float(form_data['opening_distance']),
|
||||
event_month=form_data['event_month'],
|
||||
room_number=form_data['room_number'],
|
||||
room_volume=float(form_data['room_volume']),
|
||||
simulation_name=form_data['simulation_name'],
|
||||
total_people=int(form_data['total_people']),
|
||||
ventilation_type=form_data['ventilation_type'],
|
||||
volume_type=form_data['volume_type'],
|
||||
windows_duration=float(form_data['windows_duration']),
|
||||
windows_frequency=float(form_data['windows_frequency']),
|
||||
window_height=float(form_data['window_height']),
|
||||
window_type=form_data['window_type'],
|
||||
window_width=float(form_data['window_width']),
|
||||
windows_number=int(form_data['windows_number']),
|
||||
window_opening_regime=form_data['window_opening_regime'],
|
||||
)
|
||||
form_data.pop('_xsrf', None)
|
||||
instance = cls(**form_data)
|
||||
instance.validate()
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def to_dict(self, form: "FormData") -> dict:
|
||||
form_dict = form.__dict__.copy()
|
||||
for attr_name in TIME_ATTRIBUTES:
|
||||
form_dict[attr_name] = time_minutes_to_string(form_dict[attr_name])
|
||||
for attr_name in BOOLEAN_ATTRIBUTES:
|
||||
form_dict[attr_name] = form_dict[attr_name] & 1
|
||||
return form_dict
|
||||
|
||||
def validate(self):
|
||||
# Validate time intervals selected by user
|
||||
time_intervals = [
|
||||
|
|
@ -604,6 +567,12 @@ WINDOWS_TYPES = {'window_sliding', 'window_hinged', 'not-applicable'}
|
|||
|
||||
COFFEE_OPTIONS_INT = {'coffee_break_0':0, 'coffee_break_1':1, 'coffee_break_2':2, 'coffee_break_4':4}
|
||||
|
||||
BOOLEAN_ATTRIBUTES = {'hepa_option', 'exposed_lunch_option', 'infected_lunch_option', 'infected_dont_have_breaks_with_exposed'}
|
||||
FLOAT_ATTRIBUTES = {'air_changes', 'air_supply', 'ceiling_height', 'floor_area', 'hepa_amount', 'opening_distance',
|
||||
'room_volume', 'windows_duration', 'windows_frequency', 'window_height', 'window_width'}
|
||||
INT_ATTRIBUTES = {'exposed_coffee_duration', 'infected_coffee_duration', 'infected_people', 'total_people', 'windows_number'}
|
||||
TIME_ATTRIBUTES = {'exposed_lunch_start', 'exposed_lunch_finish', 'exposed_start', 'exposed_finish',
|
||||
'infected_lunch_start', 'infected_lunch_finish', 'infected_start', 'infected_finish'}
|
||||
|
||||
def time_string_to_minutes(time: str) -> minutes_since_midnight:
|
||||
"""
|
||||
|
|
@ -612,3 +581,11 @@ def time_string_to_minutes(time: str) -> minutes_since_midnight:
|
|||
:return: The number of minutes between 'time' and 00:00
|
||||
"""
|
||||
return minutes_since_midnight(60 * int(time[:2]) + int(time[3:]))
|
||||
|
||||
def time_minutes_to_string(time: int) -> str:
|
||||
"""
|
||||
Converts time from an integer number of minutes after 00:00 to string-format
|
||||
:param time: The number of minutes between 'time' and 00:00
|
||||
:return: A string of the form "HH:MM" representing a time of day
|
||||
"""
|
||||
return "{0:0=2d}".format(int(time/60)) + ":" + "{0:0=2d}".format(time%60)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import io
|
|||
from pathlib import Path
|
||||
import typing
|
||||
|
||||
import qrcode
|
||||
import urllib
|
||||
import jinja2
|
||||
import matplotlib
|
||||
matplotlib.use('agg')
|
||||
|
|
@ -61,16 +63,46 @@ def calculate_report_data(model: models.ExposureModel):
|
|||
"emission_rate": er,
|
||||
"exposed_occupants": exposed_occupants,
|
||||
"expected_new_cases": expected_new_cases,
|
||||
"scenario_plot_src": embed_figure(plot(times, concentrations, model)),
|
||||
"scenario_plot_src": img2base64(_figure2bytes(plot(times, concentrations, model))),
|
||||
"repeated_events": repeated_events,
|
||||
}
|
||||
|
||||
|
||||
def embed_figure(figure) -> str:
|
||||
# Draw the scenario graph.
|
||||
img_data = io.BytesIO()
|
||||
def generate_qr_code(prefix, form: FormData):
|
||||
form_dict = FormData.to_dict(form)
|
||||
url = prefix + "?" + urllib.parse.urlencode(form_dict)
|
||||
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_H,
|
||||
box_size=10,
|
||||
border=4,
|
||||
)
|
||||
qr.add_data(url)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white").convert('RGB')
|
||||
|
||||
return {
|
||||
'image': img2base64(_img2bytes(img)),
|
||||
'link': url,
|
||||
}
|
||||
|
||||
|
||||
def _img2bytes(figure):
|
||||
# Draw the image
|
||||
img_data = io.BytesIO()
|
||||
figure.save(img_data, format='png', bbox_inches="tight")
|
||||
return img_data
|
||||
|
||||
|
||||
def _figure2bytes(figure):
|
||||
# Draw the image
|
||||
img_data = io.BytesIO()
|
||||
figure.savefig(img_data, format='png', bbox_inches="tight")
|
||||
return img_data
|
||||
|
||||
|
||||
def img2base64(img_data) -> str:
|
||||
plt.close()
|
||||
img_data.seek(0)
|
||||
pic_hash = base64.b64encode(img_data.read()).decode('ascii')
|
||||
|
|
@ -220,12 +252,12 @@ def comparison_report(scenarios: typing.Dict[str, models.ExposureModel]):
|
|||
'expected_new_cases': model.expected_new_cases(),
|
||||
}
|
||||
return {
|
||||
'plot': embed_figure(comparison_plot(scenarios)),
|
||||
'plot': img2base64(_figure2bytes(comparison_plot(scenarios))),
|
||||
'stats': statistics,
|
||||
}
|
||||
|
||||
|
||||
def build_report(model: models.ExposureModel, form: FormData):
|
||||
def build_report(base_url: str, model: models.ExposureModel, form: FormData):
|
||||
now = datetime.now()
|
||||
time = now.strftime("%d/%m/%Y %H:%M:%S")
|
||||
request = {"the": "form", "request": "data"}
|
||||
|
|
@ -240,6 +272,7 @@ def build_report(model: models.ExposureModel, form: FormData):
|
|||
context.update(calculate_report_data(model))
|
||||
alternative_scenarios = manufacture_alternative_scenarios(form)
|
||||
context['alternative_scenarios'] = comparison_report(alternative_scenarios)
|
||||
context['qr_code'] = generate_qr_code(f'{base_url}/calculator', form)
|
||||
|
||||
cara_templates = Path(__file__).parent.parent / "templates"
|
||||
calculator_templates = Path(__file__).parent / "templates"
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ function on_ventilation_type_change() {
|
|||
unrequire_fields(this);
|
||||
|
||||
//Clear invalid inputs for this newly hidden child element
|
||||
removeInvalid(getChildElement($(this)).find('input').not('input[type=radio]').attr('id'));
|
||||
removeInvalid("#"+getChildElement($(this)).find('input').not('input[type=radio]').attr('id'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -456,6 +456,32 @@ function parseTimeToMins(cTime) {
|
|||
|
||||
/* -------On Load------- */
|
||||
$(document).ready(function () {
|
||||
|
||||
//Pre-fill form with known values
|
||||
(new URL(decodeURIComponent(window.location.href))).searchParams.forEach((value, name) => {
|
||||
|
||||
//If element exists
|
||||
if(document.getElementsByName(name).length > 0) {
|
||||
var elemObj = document.getElementsByName(name)[0];
|
||||
|
||||
//Pre-select checked radios
|
||||
if (elemObj.type === 'radio') {
|
||||
if (value !== 'not-applicable') {
|
||||
$('[name="'+name+'"][value="'+value+'"]').prop('checked',true);
|
||||
}
|
||||
}
|
||||
//Pre-select checkboxes
|
||||
else if (elemObj.type === 'checkbox') {
|
||||
elemObj.checked = (value==1);
|
||||
}
|
||||
//Ignore 0 (default) values from server side
|
||||
else if (!(elemObj.classList.contains("non_zero") || elemObj.classList.contains("remove_zero")) || (value != "0.0" && value != "0")) {
|
||||
elemObj.value = value;
|
||||
validateValue(elemObj);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// When the document is ready, deal with the fact that we may be here
|
||||
// as a result of a forward/back browser action. If that is the case, update
|
||||
// the visibility of some of our inputs.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{% extends "layout.html.j2" %}
|
||||
|
||||
{% set MODEL_VERSION="v1.4.0" %}
|
||||
{% set MODEL_VERSION="v1.4.1" %}
|
||||
{% set DEBUG=False %}
|
||||
{% set active_page="calculator/" %}
|
||||
|
||||
|
|
@ -92,8 +92,8 @@
|
|||
<label for="windows_open_permanently">Permanently</label><br>
|
||||
<span class="tabbed"><input type="radio" id="windows_open_periodically" name="window_opening_regime" value="windows_open_periodically" onclick="require_fields(this)"></span>
|
||||
<label for="windows_open_periodically">Periodically:</label>
|
||||
<input type="number" step="any" id="windows_duration" class="disabled" name="windows_duration" placeholder="Duration (min)" min="1" data-has-radio="#windows_open_periodically"> /
|
||||
<input type="number" step="any" id="windows_frequency" class="disabled" name="windows_frequency" placeholder="Frequency (min)" min="1" data-has-radio="#windows_open_periodically">
|
||||
<input type="number" step="any" id="windows_duration" class="non_zero disabled" name="windows_duration" placeholder="Duration (min)" min="1" data-has-radio="#windows_open_periodically"> /
|
||||
<input type="number" step="any" id="windows_frequency" class="non_zero disabled" name="windows_frequency" placeholder="Frequency (min)" min="1" data-has-radio="#windows_open_periodically">
|
||||
<br>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -265,33 +265,36 @@ In this scenario, the estimated probability of one exposed occupant getting infe
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</p>
|
||||
<div style="clear: both;">
|
||||
|
||||
<p class="data_text"> <strong> Notes for alternative scenarios: </strong><br>
|
||||
<ol>
|
||||
<li>This graph shows the concentration of infectious quanta in the air. The filtration of Type I and FFP2 masks, if worn, applies not only to the emission rate but also to the individual exposure (i.e. inhalation).
|
||||
For this reason, scenarios with different types of mask will show the same concentration on the graph but have different Pi values.</li>
|
||||
<li>If you have selected more sophisticated options, such as HEPA filtration or FFP2 masks, this will be indicated in the plot as the "base scenario", representing the inputs inserted in the form.<br>
|
||||
The other alternative scenarios shown for comparison will not include either HEPA filtration or FFP2 masks.</li>
|
||||
</ol>
|
||||
<br>
|
||||
</p>
|
||||
<ol>
|
||||
<li>This graph shows the concentration of infectious quanta in the air. The filtration of Type I and FFP2 masks, if worn, applies not only to the emission rate but also to the individual exposure (i.e. inhalation).
|
||||
For this reason, scenarios with different types of mask will show the same concentration on the graph but have different Pi values.</li>
|
||||
<li>If you have selected more sophisticated options, such as HEPA filtration or FFP2 masks, this will be indicated in the plot as the "base scenario", representing the inputs inserted in the form.<br>
|
||||
The other alternative scenarios shown for comparison will not include either HEPA filtration or FFP2 masks.</li>
|
||||
</ol>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
<p class="data_text">
|
||||
<strong> Footnotes for ALARA: </strong><br>
|
||||
ALARA stands for As Low As Reasonably Achievable. It can be summarised based on 3 main points:
|
||||
<ol>
|
||||
<li>Justification - any exposure of persons has to be justified </li>
|
||||
<li>Limitation - the personal doses have to be kept below the legal limits (in this case the CERN exposure limits)</li>
|
||||
<li>Optimisation - the personal doses and collective doses have to be kept as low as reasonably achievable (ALARA).</li>
|
||||
</ol>
|
||||
For more information, please refer to <a href="https://cds.cern.ch/record/1533023/files/CERN-2013-001-p415.pdf"> this document from CERN HSE </a> and <a href="https://www.cdc.gov/nceh/radiation/safety.html#:~:text=ALARA%20stands%20for%20%E2%80%9Cas%20low,time%2C%20distance%2C%20and%20shielding."> this publication from the CDC.</a> <br>
|
||||
<p class="data_text">
|
||||
<strong> Footnotes for ALARA: </strong><br>
|
||||
ALARA stands for As Low As Reasonably Achievable. It can be summarised based on 3 main points:
|
||||
<ol>
|
||||
<li>Justification - any exposure of persons has to be justified </li>
|
||||
<li>Limitation - the personal doses have to be kept below the legal limits (in this case the CERN exposure limits)</li>
|
||||
<li>Optimisation - the personal doses and collective doses have to be kept as low as reasonably achievable (ALARA).</li>
|
||||
</ol>
|
||||
For more information, please refer to <a href="https://cds.cern.ch/record/1533023/files/CERN-2013-001-p415.pdf"> this document from CERN HSE </a> and <a href="https://www.cdc.gov/nceh/radiation/safety.html#:~:text=ALARA%20stands%20for%20%E2%80%9Cas%20low,time%2C%20distance%2C%20and%20shielding."> this publication from the CDC.</a>
|
||||
<br><br>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<a href="{{ qr_code.link }}"><img style="width:250pt;" id="qr_code" src="{{ qr_code.image }}"/></a>
|
||||
<i style="position:absolute; margin-top:3.5cm">Click the QR code to regenerate the report and get a shareable link.<br>Alternatively, scan to regenerate the report.<br> Mobile-friendly app coming soon!</i>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
</p>
|
||||
|
||||
<br><br><br>
|
||||
<div style="border: 2px solid black; padding: 15px;">
|
||||
<p class="image"> <img <img align="middle" src="/calculator/static/images/disclaimer.jpg" width="40" height="40"><b>Disclaimer:</b><br><br></p>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ def baseline_form(baseline_form_data):
|
|||
def test_generate_report(baseline_form):
|
||||
model = baseline_form.build_model()
|
||||
|
||||
report = report_generator.build_report(model, baseline_form)
|
||||
report = report_generator.build_report("", model, baseline_form)
|
||||
assert report != ""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,64 +3,73 @@
|
|||
# pip freeze | grep -v cara >> requirements.txt
|
||||
|
||||
.[app]
|
||||
anyio==2.1.0
|
||||
appnope==0.1.2
|
||||
argon2-cffi==20.1.0
|
||||
async-generator==1.10
|
||||
attrs==20.2.0
|
||||
attrs==20.3.0
|
||||
backcall==0.2.0
|
||||
bleach==3.2.1
|
||||
certifi==2020.6.20
|
||||
cffi==1.14.3
|
||||
bleach==3.3.0
|
||||
certifi==2020.12.5
|
||||
cffi==1.14.5
|
||||
contextvars==2.4
|
||||
cycler==0.10.0
|
||||
dataclasses==0.7
|
||||
dataclasses==0.8
|
||||
decorator==4.4.2
|
||||
defusedxml==0.6.0
|
||||
entrypoints==0.3
|
||||
importlib-metadata==2.0.0
|
||||
ipykernel==5.3.4
|
||||
ipympl==0.5.8
|
||||
idna==3.1
|
||||
immutables==0.15
|
||||
importlib-metadata==3.5.0
|
||||
ipykernel==5.5.0
|
||||
ipympl==0.6.3
|
||||
ipython==7.16.1
|
||||
ipython-genutils==0.2.0
|
||||
ipywidgets==7.5.1
|
||||
jedi==0.17.2
|
||||
Jinja2==2.11.2
|
||||
ipywidgets==7.6.3
|
||||
jedi==0.18.0
|
||||
Jinja2==2.11.3
|
||||
jsonschema==3.2.0
|
||||
jupyter-client==6.1.7
|
||||
jupyter-core==4.6.3
|
||||
jupyter-server==1.0.5
|
||||
jupyter-client==6.1.11
|
||||
jupyter-core==4.7.1
|
||||
jupyter-server==1.4.1
|
||||
jupyterlab-pygments==0.1.2
|
||||
kiwisolver==1.2.0
|
||||
jupyterlab-widgets==1.0.0
|
||||
kiwisolver==1.3.1
|
||||
MarkupSafe==1.1.1
|
||||
matplotlib==3.3.2
|
||||
matplotlib==3.3.4
|
||||
mistune==0.8.4
|
||||
nbclient==0.5.1
|
||||
nbclient==0.5.2
|
||||
nbconvert==6.0.7
|
||||
nbformat==5.0.8
|
||||
nest-asyncio==1.4.1
|
||||
notebook==6.1.4
|
||||
numpy==1.19.2
|
||||
packaging==20.4
|
||||
pandocfilters==1.4.2
|
||||
parso==0.7.1
|
||||
nbformat==5.1.2
|
||||
nest-asyncio==1.5.1
|
||||
notebook==6.2.0
|
||||
numpy==1.19.5
|
||||
packaging==20.9
|
||||
pandocfilters==1.4.3
|
||||
parso==0.8.1
|
||||
pexpect==4.8.0
|
||||
pickleshare==0.7.5
|
||||
Pillow==8.0.0
|
||||
prometheus-client==0.8.0
|
||||
prompt-toolkit==3.0.8
|
||||
ptyprocess==0.6.0
|
||||
Pillow==8.1.0
|
||||
prometheus-client==0.9.0
|
||||
prompt-toolkit==3.0.16
|
||||
ptyprocess==0.7.0
|
||||
pycparser==2.20
|
||||
Pygments==2.7.1
|
||||
Pygments==2.8.0
|
||||
pyparsing==2.4.7
|
||||
pyrsistent==0.17.3
|
||||
python-dateutil==2.8.1
|
||||
pyzmq==19.0.2
|
||||
pyzmq==22.0.3
|
||||
qrcode==6.1
|
||||
Send2Trash==1.5.0
|
||||
six==1.15.0
|
||||
terminado==0.9.1
|
||||
sniffio==1.2.0
|
||||
terminado==0.9.2
|
||||
testpath==0.4.4
|
||||
tornado==6.0.4
|
||||
tornado==6.1
|
||||
traitlets==4.3.3
|
||||
voila==0.2.4
|
||||
typing-extensions==3.7.4.3
|
||||
voila==0.2.7
|
||||
wcwidth==0.2.5
|
||||
webencodings==0.5.1
|
||||
widgetsnbextension==3.5.1
|
||||
zipp==3.3.1
|
||||
zipp==3.4.0
|
||||
|
|
|
|||
|
|
@ -16,3 +16,6 @@ ignore_missing_imports = True
|
|||
|
||||
[mypy-mistune.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-qrcode.*]
|
||||
ignore_missing_imports = True
|
||||
|
|
|
|||
1
setup.py
1
setup.py
|
|
@ -24,6 +24,7 @@ REQUIREMENTS: dict = {
|
|||
'matplotlib',
|
||||
'mistune',
|
||||
'numpy',
|
||||
'qrcode[pil]',
|
||||
'tornado',
|
||||
'voila >=0.2.4',
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in a new issue