summaryrefslogtreecommitdiffhomepage
path: root/gui/scripts
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2019-10-15 12:17:47 +0200
committerAndrej Mihajlov <and@mullvad.net>2019-10-15 13:44:22 +0200
commit9bf06a8e8833e9a95b6bac95d57533f2ba724bda (patch)
treeb34880af2dd3d3a2c9094f4857b184b6fce77a75 /gui/scripts
parent2156081a3b5391aac5fa3fe9fa27dcedf9d5b4ba (diff)
downloadmullvadvpn-9bf06a8e8833e9a95b6bac95d57533f2ba724bda.tar.xz
mullvadvpn-9bf06a8e8833e9a95b6bac95d57533f2ba724bda.zip
Convert to python 3
Diffstat (limited to 'gui/scripts')
-rw-r--r--gui/scripts/README.md25
-rw-r--r--gui/scripts/extract-geo-data.py114
-rw-r--r--gui/scripts/integrate-into-app.py14
-rw-r--r--gui/scripts/requirements.txt79
4 files changed, 136 insertions, 96 deletions
diff --git a/gui/scripts/README.md b/gui/scripts/README.md
index 8d94a47687..6d11c55ec0 100644
--- a/gui/scripts/README.md
+++ b/gui/scripts/README.md
@@ -1,10 +1,10 @@
-This is a folder with the supporting scripts written in python 2, node, bash.
+This is a folder with the supporting scripts written in Python 3, node, bash.
## Dependency installation notes
1. Run the following command in terminal to install python dependencies:
- `pip install -r requirements.txt`
+ `pip3 install -r requirements.txt`
2. Run `npm install -g topojson-server` to install `geo2topo` tool which is
used by python scripts to convert GeoJSON to TopoJSON
@@ -53,7 +53,7 @@ unzip ne_50m_populated_places.zip -d ne_50m_populated_places/
Run the following script to produce a TopoJSON data used by the app:
```
-python extract-geo-data.py
+python3 extract-geo-data.py
```
and finally generate the R-Tree cache:
@@ -70,5 +70,22 @@ Once you've extracted all the geo data, run the integration script that will
copy all files ignoring intermediate ones into the `gui/assets/geo` folder:
```
-python integrate-into-app.py
+python3 integrate-into-app.py
+```
+
+## Locking Python dependencies
+
+1. Freeze dependencies:
+
+```
+pip3 freeze -r requirements.txt
+```
+
+and save the output into `requirements.txt`.
+
+
+2. Hash them with `hashin` tool:
+
+```
+hashin --python 3.7 --verbose --update-all
```
diff --git a/gui/scripts/extract-geo-data.py b/gui/scripts/extract-geo-data.py
index 611e6acffd..e64cb68867 100644
--- a/gui/scripts/extract-geo-data.py
+++ b/gui/scripts/extract-geo-data.py
@@ -5,7 +5,7 @@ This module forms a geo json of highly populated cities in the world
import os
from os import path
import json
-import urllib2
+import urllib.request
from subprocess import Popen, PIPE
from polib import POFile, POEntry
import colorful as c
@@ -74,7 +74,7 @@ def extract_cities():
with open(output_path, "w") as f:
f.write(json.dumps(my_layer))
- print c.green("Extracted data to {}".format(output_path))
+ print(c.green("Extracted data to {}".format(output_path)))
def extract_countries():
@@ -106,7 +106,7 @@ def extract_countries():
with open(output_path, "w") as f:
f.write(json.dumps(my_layer))
- print c.green("Extracted data to {}".format(output_path))
+ print(c.green("Extracted data to {}".format(output_path)))
def extract_geometry():
@@ -130,11 +130,11 @@ def extract_geometry():
['geo2topo', '-q', '1e5', 'geometry=-', '-o', output_path],
stdin=PIPE, stdout=PIPE, stderr=PIPE
)
- errors = p.communicate(input=json.dumps(my_layer))[1]
+ errors = p.communicate(input=json.dumps(my_layer).encode())[1]
if p.returncode == 0:
- print c.green("Extracted data to {}".format(output_path))
+ print(c.green("Extracted data to {}".format(output_path)))
else:
- print c.red("geo2topo exited with {}. {}".format(p.returncode, errors.decode('utf-8').strip()))
+ print(c.red("geo2topo exited with {}. {}".format(p.returncode, errors.decode().strip())))
def extract_provinces_and_states_lines():
@@ -158,11 +158,11 @@ def extract_provinces_and_states_lines():
['geo2topo', '-q', '1e5', 'geometry=-', '-o', output_path],
stdin=PIPE, stdout=PIPE, stderr=PIPE
)
- errors = p.communicate(input=json.dumps(my_layer))[1]
+ errors = p.communicate(input=json.dumps(my_layer).encode())[1]
if p.returncode == 0:
- print c.green("Extracted data to {}".format(output_path))
+ print(c.green("Extracted data to {}".format(output_path)))
else:
- print c.red("geo2topo exited with {}. {}".format(p.returncode, errors.decode('utf-8').strip()))
+ print(c.red("geo2topo exited with {}. {}".format(p.returncode, errors.decode().strip())))
def extract_countries_po():
@@ -181,7 +181,7 @@ def extract_countries_po():
if not path.exists(locale_out_dir):
os.makedirs(locale_out_dir)
- print "Generating {}".format(output_path)
+ print("Generating {}".format(output_path))
for feat in source:
props = lower_dict_keys(feat["properties"])
@@ -196,11 +196,11 @@ def extract_countries_po():
translated_name = props.get(name_key)
elif props.get(name_fallback) is not None:
translated_name = props.get(name_fallback)
- print c.orange(u"Missing translation for {}".format(translated_name))
+ print(c.orange("Missing translation for {}".format(translated_name)))
else:
raise ValueError(
"Cannot find the translation for {}. Probe keys: {}"
- .format(locale, (name_key, name_fallback))
+ .format(locale, (name_key, name_fallback))
)
entry = POEntry(
@@ -235,7 +235,7 @@ def extract_countries_po():
sort_pofile_entries(po)
po.save(output_path)
- print c.green("Extracted {} countries for {} to {}".format(len(po), locale, output_path))
+ print(c.green("Extracted {} countries for {} to {}".format(len(po), locale, output_path)))
def extract_cities_po():
@@ -256,7 +256,7 @@ def extract_cities_po():
if not path.exists(locale_out_dir):
os.makedirs(locale_out_dir)
- print "Generating {}".format(output_path)
+ print("Generating {}".format(output_path))
with fiona.open(input_path) as source:
for feat in source:
@@ -271,12 +271,12 @@ def extract_cities_po():
hits += 1
elif props.get(name_fallback) is not None:
translated_name = props.get(name_fallback)
- print c.orange(u"Missing translation for {}".format(translated_name))
+ print(c.orange("Missing translation for {}".format(translated_name)))
misses += 1
else:
raise ValueError(
"Cannot find the translation for {}. Probe keys: {}"
- .format(locale, (name_key, name_fallback))
+ .format(locale, (name_key, name_fallback))
)
entry = POEntry(
@@ -287,11 +287,11 @@ def extract_cities_po():
try:
po.append(entry)
except ValueError as err:
- print c.orange(u"Cannot add an entry: {}".format(err))
+ print(c.orange("Cannot add an entry: {}".format(err)))
sort_pofile_entries(po)
po.save(output_path)
- print c.green("Extracted {} cities to {}".format(len(po), output_path))
+ print(c.green("Extracted {} cities to {}".format(len(po), output_path)))
stats.append((locale, hits, misses))
@@ -299,14 +299,14 @@ def extract_cities_po():
def sort_pofile_entries(pofile):
- pofile.sort(key=lambda o: o.msgid_with_context.encode('utf-8'))
+ pofile.sort(key=lambda o: o.msgid_with_context)
def extract_relay_translations():
try:
response = request_relays()
except Exception as e:
- print c.red("Failed to fetch the relays list: {}".format(e))
+ print(c.red("Failed to fetch the relays list: {}".format(e)))
raise
result = response.get("result")
@@ -326,18 +326,18 @@ def extract_relay_locations_pot(countries):
pot.metadata = {"Content-Type": "text/plain; charset=utf-8"}
output_path = path.join(LOCALE_OUT_DIR, RELAY_LOCATIONS_POT_FILENAME)
- print "Generating {}".format(output_path)
+ print("Generating {}".format(output_path))
for country in countries:
country_name = country.get("name")
if country_name is not None:
entry = POEntry(
msgid=country_name,
- msgstr=u"",
+ msgstr="",
comment=country.get("code").upper()
)
pot.append(entry)
- print u"{} ({})".format(country_name, country.get("code")).encode('utf-8')
+ print("{} ({})".format(country_name, country.get("code")))
cities = country.get("cities")
if cities is not None:
@@ -346,16 +346,16 @@ def extract_relay_locations_pot(countries):
if city_name is not None:
entry = POEntry(
msgid=city_name,
- msgstr=u"",
- comment=u"{} {}".format(country.get("code").upper(), city.get("code").upper())
+ msgstr="",
+ comment="{} {}".format(country.get("code").upper(), city.get("code").upper())
)
try:
pot.append(entry)
except ValueError as err:
- print c.orange(u"Cannot add an entry: {}".format(err))
+ print(c.orange("Cannot add an entry: {}".format(err)))
- print u"{} ({})".format(city_name, city.get("code")).encode('utf-8')
+ print("{} ({})".format(city_name, city.get("code")))
pot.save(output_path)
@@ -374,7 +374,7 @@ def prepare_stats_table_column(item):
def print_stats_table(title, data):
header = ("Locale", "Hits", "Misses", "% translated", "Total")
- color_data = map(prepare_stats_table_column, data)
+ color_data = list(map(prepare_stats_table_column, data))
table = AsciiTable([header] + color_data)
table.title = title
@@ -382,9 +382,9 @@ def print_stats_table(title, data):
for i in range(1, 5):
table.justify_columns[i] = 'center'
- print ""
- print table.table
- print ""
+ print("")
+ print(table.table)
+ print("")
def translate_relay_locations(countries):
@@ -403,7 +403,7 @@ def translate_relay_locations(countries):
for locale in os.listdir(LOCALE_DIR):
locale_dir = path.join(LOCALE_DIR, locale)
if path.isdir(locale_dir):
- print "Generating {}".format(path.join(locale, RELAY_LOCATIONS_PO_FILENAME))
+ print("Generating {}".format(path.join(locale, RELAY_LOCATIONS_PO_FILENAME)))
(hits, misses) = translate_single_relay_locations(country_translator, city_translator, countries, locale)
stats.append((locale, hits, misses))
@@ -442,12 +442,11 @@ def translate_single_relay_locations(country_translator, city_translator, countr
translated_country_name = ""
misses += 1
- log_message = u"{} ({}) -> \"{}\"".format(
- country_name, country_code, translated_country_name).encode('utf-8')
+ log_message = "{} ({}) -> \"{}\"".format(country_name, country_code, translated_country_name)
if found_country_translation:
- print c.green(log_message)
+ print(c.green(log_message))
else:
- print c.orange(log_message)
+ print(c.orange(log_message))
# translate country
entry = POEntry(
@@ -460,8 +459,8 @@ def translate_single_relay_locations(country_translator, city_translator, countr
# translate cities
cities = country.get("cities")
if cities is None:
- print c.orange(u"Skip {} ({}) because no cities were found.".format(
- country_name, country_code))
+ print(c.orange("Skip {} ({}) because no cities were found."
+ .format(country_name, country_code)))
continue
for city in cities:
@@ -476,7 +475,7 @@ def translate_single_relay_locations(country_translator, city_translator, countr
translated_name = city_translator.translate(locale, split[0].strip())
if translated_name is not None and len(split) > 1:
- translated_name = u"{}, {}".format(translated_name, split[1].strip())
+ translated_name = "{}, {}".format(translated_name, split[1].strip())
else:
translated_name = city_translator.translate(locale, city_name)
@@ -488,23 +487,22 @@ def translate_single_relay_locations(country_translator, city_translator, countr
translated_name = ""
misses += 1
- log_message = u"{} ({}) -> \"{}\"".format(
- city_name, city_code, translated_name).encode('utf-8')
+ log_message = "{} ({}) -> \"{}\"".format(city_name, city_code, translated_name)
if found_translation:
- print c.green(log_message)
+ print(c.green(log_message))
else:
- print c.orange(log_message)
+ print(c.orange(log_message))
entry = POEntry(
msgid=city_name,
msgstr=translated_name,
- comment=u"{} {}".format(country_code.upper(), city_code.upper())
+ comment="{} {}".format(country_code.upper(), city_code.upper())
)
try:
po.append(entry)
except ValueError as err:
- print c.orange(u"Cannot add an entry: {}".format(err))
+ print(c.orange("Cannot add an entry: {}".format(err)))
po.save(output_path)
@@ -513,14 +511,12 @@ def translate_single_relay_locations(country_translator, city_translator, countr
### HELPERS ###
-class CountryTranslator(object):
+class CountryTranslator:
"""
This class provides facilities for translating countries
"""
def __init__(self):
- super(CountryTranslator, self).__init__()
-
self.dataset = self.__build_index()
def translate(self, locale, iso_a2):
@@ -539,8 +535,8 @@ class CountryTranslator(object):
value = props.get(name_key)
if value is None:
- print c.orange(u"Missing translation for {} ({}) under the {} key".format(
- iso_a2, locale, name_key).encode('utf-8'))
+ print(c.orange("Missing translation for {} ({}) under the {} key"
+ .format(iso_a2, locale, name_key)))
else:
return value
@@ -567,14 +563,12 @@ class CountryTranslator(object):
return dataset
-class CityTranslator(object):
+class CityTranslator:
"""
This class provides facilities for translating places from English.
"""
def __init__(self):
- super(CityTranslator, self).__init__()
-
self.dataset = self.__build_index()
def translate(self, locale, english_name):
@@ -594,8 +588,8 @@ class CityTranslator(object):
value = props.get(name_key)
if value is None:
- print c.orange(u"Missing translation for {} ({}) under the {} key".format(
- english_name, locale, name_key).encode('utf-8'))
+ print(c.orange("Missing translation for {} ({}) under the {} key"
+ .format(english_name, locale, name_key)))
else:
return value
@@ -639,7 +633,7 @@ def get_shape_path(dataset_name):
def lower_dict_keys(input_dict):
- return dict((k.lower(), v) for k, v in input_dict.iteritems())
+ return dict((k.lower(), v) for k, v in input_dict.items())
def convert_locale_ident(locale_ident):
@@ -664,10 +658,10 @@ def map_locale(locale_ident):
def request_relays():
- data = json.dumps({"jsonrpc": "2.0", "id": "0", "method": "relay_list_v3"})
- headers = {"Content-Type": "application/json"}
- request = urllib2.Request("https://api.mullvad.net/rpc/", data, headers)
- return json.load(urllib2.urlopen(request))
+ data = json.dumps({"jsonrpc": "2.0", "id": "0", "method": "relay_list_v3"}).encode()
+ request = urllib.request.Request("https://api.mullvad.net/rpc/", data=data)
+ request.add_header("Content-Type", "application/json")
+ return json.load(urllib.request.urlopen(request))
# Program main()
diff --git a/gui/scripts/integrate-into-app.py b/gui/scripts/integrate-into-app.py
index ce94da537c..28137a2732 100644
--- a/gui/scripts/integrate-into-app.py
+++ b/gui/scripts/integrate-into-app.py
@@ -52,7 +52,7 @@ def copy_geo_assets():
src = path.join(GENERATED_CONTENT_OUTPUT_PATH, f)
dst = path.join(APP_GEO_ASSETS_PATH, f)
- print u"Copying {} to {}".format(src, dst)
+ print("Copying {} to {}".format(src, dst))
shutil.copyfile(src, dst)
@@ -79,14 +79,14 @@ def merge_single_locale_folder(src, dst):
dst_po = path.join(dst, f)
if f in TRANSLATIONS_TO_COPY:
- print u"Copying {} to {}".format(src_po, dst_po)
+ print("Copying {} to {}".format(src_po, dst_po))
shutil.copyfile(src_po, dst_po)
elif f in TRANSLATIONS_TO_MERGE:
# merge ../locales/*/file.po with ./out/locales/*/file.po
# use existing translation to resolve conflicts
merge_gettext_catalogues(dst_po, src_po)
else:
- print c.orange(u"Unexpected file: {}".format(src_po))
+ print(c.orange("Unexpected file: {}".format(src_po)))
def merge_gettext_catalogues(existing_catalogue_file, generated_catalogue_file):
@@ -109,11 +109,11 @@ def merge_gettext_catalogues(existing_catalogue_file, generated_catalogue_file):
(exit_code, errors) = run_program("msgcat", *args)
if exit_code == 0:
- print c.green(u"Merged {} into {}.".format(generated_catalogue_file, existing_catalogue_file))
+ print(c.green("Merged {} into {}.".format(generated_catalogue_file, existing_catalogue_file)))
else:
- print c.red(u"msgcat exited with {}: {}".format(exit_code, errors.decode('utf-8').strip()))
+ print(c.red("msgcat exited with {}: {}".format(exit_code, errors.decode().strip())))
else:
- print c.orange(u"The existing catalogue does not exist. Copying {} to {}"
+ print(c.orange("The existing catalogue does not exist. Copying {} to {}")
.format(generated_catalogue_file, existing_catalogue_file))
shutil.copyfile(generated_catalogue_file, existing_catalogue_file)
@@ -121,7 +121,7 @@ def merge_gettext_catalogues(existing_catalogue_file, generated_catalogue_file):
def run_program(*args):
p = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
- print u"Run: {}".format(' '.join(args))
+ print("Run: {}".format(' '.join(args)))
errors = p.communicate()[1]
return (p.returncode, errors)
diff --git a/gui/scripts/requirements.txt b/gui/scripts/requirements.txt
index 4693c81bf8..2624dd6b14 100644
--- a/gui/scripts/requirements.txt
+++ b/gui/scripts/requirements.txt
@@ -1,33 +1,62 @@
-Fiona==1.8.6 \
- --hash=sha256:1353a0c7d03f96b0755d13bc0277b0b6a79585f94981e1ae6452c55958431b2b \
- --hash=sha256:1783c3abceccca318fac3d5bfd4305bc31c94fbb5abdc60e96b0ccedaa263857 \
- --hash=sha256:22dfc9d82adae78567b2a242f297915aeb7727cde06a3d762252e5393af959c5 \
- --hash=sha256:2e1ba6e87f9a63962af4c4c44152f9f49245c29fd90ce1c4bb19bddbb31e22f4 \
- --hash=sha256:6c2b665ad369768cde426def664a9af46263152731b4a9493380a337f6c5a79e \
- --hash=sha256:7ebfdce0be685aa44c31529d57cdea232c2386ad305c11a329f91c7613dbd2f4 \
- --hash=sha256:8e361b3cf00a472570fdd25ad39c722f1b3fda03fc4de1d35aac2b5da39bcdb3 \
- --hash=sha256:cb9081003e8585a284365b28cff61343541405263fd4e1c79f3baec7321b8b3f \
- --hash=sha256:e3bfabe62c4081c88c47aa10c6165ecfefaa7c9aa5406496b75ff364757a0bbe \
- --hash=sha256:ee2bbe5640f68342b6746a0ba733e25ee221106368558be6cd73dd08fc6fbe04 \
- --hash=sha256:fa31dfe8855b9cd0b128b47a4df558f1b8eda90d2181bff1dd9854e5556efb3e
+Fiona==1.8.8 \
+ --hash=sha256:2bc68b11580352a10a92da734dc09bd12348e45188d94416788681430ff0f02a \
+ --hash=sha256:711c3be73203b37812992089445a1e4e9d3d6b64e667389f7b15406e15a91e83 \
+ --hash=sha256:8431aa3d790c5b509c4532ddb9a783df34d02a4e7b3175e5e75d058c09f81a90
Shapely==1.6.4.post2 \
- --hash=sha256:0378964902f89b8dbc332e5bdfa08e0bc2f7ab39fecaeb17fbb2a7699a44fe71 \
- --hash=sha256:34e7c6f41fb27906ccdf2514ee44a5774b90b39a256b6511a6a57d11ffe64999 \
- --hash=sha256:3ca69d4b12e2b05b549465822744b6a3a1095d8488cc27b2728a06d3c07d0eee \
- --hash=sha256:3e9388f29bd81fcd4fa5c35125e1fbd4975ee36971a87a90c093f032d0e9de24 \
--hash=sha256:3ef28e3f20a1c37f5b99ea8cf8dcb58e2f1a8762d65ed2d21fd92bf1d4811182 \
- --hash=sha256:523c94403047eb6cacd7fc1863ebef06e26c04d8a4e7f8f182d49cd206fe787e \
- --hash=sha256:5d22a1a705c2f70f61ccadc696e33d922c1a92e00df8e1d58a6ade14dd7e3b4f \
- --hash=sha256:714b6680215554731389a1bbdae4cec61741aa4726921fa2b2b96a6f578a2534 \
--hash=sha256:7dfe1528650c3f0dc82f41a74cf4f72018288db9bfb75dcd08f6f04233ec7e78 \
- --hash=sha256:ba58b21b9cf3c33725f7f530febff9ed6a6846f9d0bf8a120fc74683ff919f89 \
- --hash=sha256:c4b87bb61fc3de59fc1f85e71a79b0c709dc68364d9584473697aad4aa13240f \
- --hash=sha256:ebb4d2bee7fac3f6c891fcdafaa17f72ab9c6480f6d00de0b2dc9a5137dfe342
+ --hash=sha256:c4b87bb61fc3de59fc1f85e71a79b0c709dc68364d9584473697aad4aa13240f
polib==1.1.0 \
--hash=sha256:93b730477c16380c9a96726c54016822ff81acfa553977fdd131f2b90ba858d7 \
--hash=sha256:fad87d13696127ffb27ea0882d6182f1a9cf8a5e2b37a587751166c51e5a332a
-colorful==0.5.0 \
- --hash=sha256:74bbaeb90fb5b3d31fccdc6e562bdce721f19dc6d566f72a31e43f5503ac2841 \
- --hash=sha256:86db34ba9b41106d7c69a394049e9d47be4cf15f863d04b8a0b21509ea9df6bc
+colorful==0.5.4 \
+ --hash=sha256:86848ad4e2eda60cd2519d8698945d22f6f6551e23e95f3f14dfbb60997807ea \
+ --hash=sha256:8d264b52a39aae4c0ba3e2a46afbaec81b0559a99be0d2cfe2aba4cf94531348
terminaltables==3.1.0 \
--hash=sha256:f3eb0eb92e3833972ac36796293ca0906e998dc3be91fbe1f8615b331b853b81
+## The following requirements were added by pip freeze:
+attrs==19.3.0 \
+ --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \
+ --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72
+cairocffi==1.1.0 \
+ --hash=sha256:f1c0c5878f74ac9ccb5d48b2601fcc75390c881ce476e79f4cfedd288b1b05db
+CairoSVG==2.4.2 \
+ --hash=sha256:4e668f96653326780036ebb0a9ff2bb59a8443d7bcfc51a14aab77b57a8e67ad \
+ --hash=sha256:9cb1df7e9bc60f75fb87f67940a8fb805aad544337a67a40b67c05cfe33711a2
+cffi==1.13.0 \
+ --hash=sha256:8fe230f612c18af1df6f348d02d682fe2c28ca0a6c3856c99599cdacae7cf226
+click==7.0 \
+ --hash=sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13 \
+ --hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7
+click-plugins==1.1.1 \
+ --hash=sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b \
+ --hash=sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8
+cligj==0.5.0 \
+ --hash=sha256:20f24ce9abfde3f758aec3399e6811b936b6772f360846c662c19bf5537b4f14 \
+ --hash=sha256:6c7d52d529a78712491974f975c33473f430c0f7beb18c0d7a402a743dcb460a
+cssselect2==0.2.2 \
+ --hash=sha256:07e9c3b1b52d81dd08b177532bbd6b9ced650d87abfd641f4e4ec7de34b98807 \
+ --hash=sha256:70485a680cd72b023f0ce5ae4dcd392e2b10f7280e20afdb1735334bd6af7e6a
+defusedxml==0.6.0 \
+ --hash=sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93 \
+ --hash=sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5
+munch==2.3.2 \
+ --hash=sha256:6ae3d26b837feacf732fb8aa5b842130da1daf221f5af9f9d4b2a0a6414b0d51
+Pillow==6.2.0 \
+ --hash=sha256:2ed9c4f694861642401f27dc3cb99772be67cd190e84845c749dae0a06c3bfae \
+ --hash=sha256:4548236844327a718ce3bb182ab32a16fa2050c61e334e959f554cac052fb0df \
+ --hash=sha256:65a28969a025a0eb4594637b6103201dc4ed2a9508bdab56ac33e43e3081c404 \
+ --hash=sha256:972a7aaeb7c4a2795b52eef52ee991ef040b31009f36deca6207a986607b55f3 \
+ --hash=sha256:9ba37698e242223f8053cc158f130aee046a96feacbeab65893dbe94f5530118 \
+ --hash=sha256:f1baa54d50ec031d1a9beb89974108f8f2c0706f49798f4777df879df0e1adb6
+pycparser==2.19 \
+ --hash=sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3
+six==1.12.0 \
+ --hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \
+ --hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73
+tinycss2==1.0.2 \
+ --hash=sha256:6427d0e3faa0a5e0e8c9f6437e2de26148a7a197a8b0992789f23d9a802788cf \
+ --hash=sha256:9fdacc0e22d344ddd2ca053837c133900fe820ae1222f63b79617490a498507a
+webencodings==0.5.1 \
+ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
+ --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923