Skip to content

Instantly share code, notes, and snippets.

@limitedeternity
Last active September 3, 2019 13:46
Show Gist options
  • Save limitedeternity/c7dffc0475e290d3bd5db32d13407683 to your computer and use it in GitHub Desktop.
Save limitedeternity/c7dffc0475e290d3bd5db32d13407683 to your computer and use it in GitHub Desktop.
Parse schedule from guide.herzen.spb.ru
{
"Н": {
"0": [
{
"Время": "9:45 — 11:20",
"Неделя": "Н",
"Подгруппа 1": "Архитектура ЭВМ [лаб] (28.10—23.12) доц. Матюшичев Илья Юрьевич, ауд. 267",
"Подгруппа 2": "Теоретические основы программирования [лаб] (2.09—9.12) доц. Кудрявцева Ирина Андреевна, ауд. 269"
},
{
"Время": "11:30 — 13:05",
"Неделя": "Н",
"Подгруппа 1": "Теоретические основы программирования [лекц] (2.09—9.12) доц. Кудрявцева Ирина Андреевна, ауд. 269",
"Подгруппа 2": "Теоретические основы программирования [лекц] (2.09—9.12) доц. Кудрявцева Ирина Андреевна, ауд. 269"
},
{
"Время": "13:30 — 15:05",
"Неделя": null,
"Подгруппа 1": "Риторика [лекц] (2.09—30.09) доц. Самойлова Елена Павловна, ауд. 335, корпус 1Архитектура ЭВМ [лекц] (21.10—16.12) доц. Матюшичев Илья Юрьевич, ауд. 365",
"Подгруппа 2": "Риторика [лекц] (2.09—30.09) доц. Самойлова Елена Павловна, ауд. 335, корпус 1Архитектура ЭВМ [лекц] (21.10—16.12) доц. Матюшичев Илья Юрьевич, ауд. 365"
},
{
"Время": "15:10 — 16:45",
"Неделя": null,
"Подгруппа 1": "Риторика [лекц] (2.09—23.09) доц. Самойлова Елена Павловна, ауд. 335, корпус 1Риторика [практ] (30.09) доц. Самойлова Елена Павловна, ауд. 335, корпус 1Архитектура ЭВМ [лаб] (21.10—23.12) доц. Матюшичев Илья Юрьевич, ауд. 267",
"Подгруппа 2": "Риторика [лекц] (2.09—23.09) доц. Самойлова Елена Павловна, ауд. 335, корпус 1Риторика [практ] (30.09) доц. Самойлова Елена Павловна, ауд. 335, корпус 1"
}
],
"1": [
{
"Время": "8:00 — 9:35",
"Неделя": "Н",
"Подгруппа 1": "—",
"Подгруппа 2": "Физика [лаб] (3.09—15.10) асс. Кононов Алексей Андреевич, ауд. 366Физика [лаб] (29.10—10.12) асс. Кононов Алексей Андреевич, ауд. 367"
},
{
"Время": "9:45 — 11:20",
"Неделя": "Н",
"Подгруппа 1": "Теоретические основы программирования [лаб] (26.11—10.12) доц. Кудрявцева Ирина Андреевна, ауд. 269",
"Подгруппа 2": "Физика [лаб] (3.09—15.10) асс. Кононов Алексей Андреевич, ауд. 366Физика [лаб] (29.10—10.12) асс. Кононов Алексей Андреевич, ауд. 367"
},
{
"Время": "11:30 — 13:05",
"Неделя": null,
"Подгруппа 1": "Физика [лекц] (3.09—17.12) проф. Серегин Павел Павлович, ауд. 237, корпус 1",
"Подгруппа 2": "Физика [лекц] (3.09—17.12) проф. Серегин Павел Павлович, ауд. 237, корпус 1"
},
{
"Время": "13:30 — 15:05",
"Неделя": null,
"Подгруппа 1": "Физическая культура и спорт (элективная дисциплина) [практ] (3.09—10.12, 9 студ.) доц. Фокин Александр Михайлович, ауд. 1, корпус 4",
"Подгруппа 2": "Физическая культура и спорт (элективная дисциплина) [практ] (3.09—10.12, 20 студ.) доц. Богданов Олег Андреевич, ауд. 1, корпус 4"
},
{
"Время": "15:10 — 16:45",
"Неделя": "Н",
"Подгруппа 1": "Архитектура ЭВМ [лаб] (29.10—24.12) доц. Матюшичев Илья Юрьевич, ауд. 267",
"Подгруппа 2": "—"
}
],
"2": [],
"3": [
{
"Время": "9:45 — 11:20",
"Неделя": "Н",
"Подгруппа 1": "Архитектура ЭВМ [лаб] (31.10—26.12) доц. Матюшичев Илья Юрьевич, ауд. 267",
"Подгруппа 2": "—"
},
{
"Время": "11:30 — 13:05",
"Неделя": null,
"Подгруппа 1": "Перевод научно-технической литературы [лаб] (5.09—19.12 англ.яз.) ст. преп. Хахалина Марина Сергеевна, ауд. 255",
"Подгруппа 2": "Перевод научно-технической литературы [лаб] (5.09—19.12, англ.язык) ст. преп. Порязь Надежда Вадимовна, ауд. 222, корпус 1Перевод научно-технической литературы фр. [лаб] (10.10 фр. яз., 24.10,фр.яз., 7.11, фр.яз., 21.11, фр.яз.) доц. Овсянников Александр Олегович, ауд. 270"
},
{
"Время": "13:30 — 15:05",
"Неделя": null,
"Подгруппа 1": "Физическая культура и спорт (элективная дисциплина) [практ] (5.09—5.12, 20 студ.) доц. Богданов Олег Андреевич, ауд. 1, корпус 4",
"Подгруппа 2": "Физическая культура и спорт (элективная дисциплина) [практ] (5.09—5.12, 9 студ.) доц. Фокин Александр Михайлович, ауд. 1, корпус 4Архитектура ЭВМ [лаб] (26.12) доц. Матюшичев Илья Юрьевич, ауд. 267"
},
{
"Время": "15:10 — 16:45",
"Неделя": null,
"Подгруппа 1": "Перевод научно-технической литературы исп. [лаб] (3.10—19.12, исп. язык) доц. Овсянников Александр Олегович, ауд. 222, корпус 1",
"Подгруппа 2": "Архитектура ЭВМ [лаб] (24.10—26.12) доц. Матюшичев Илья Юрьевич, ауд. 267"
},
{
"Время": "16:50 — 18:25",
"Неделя": null,
"Подгруппа 1": "—",
"Подгруппа 2": "Перевод научно-технической литературы фр. [лаб] (3.10—19.12 фр. яз.) доц. Овсянников Александр Олегович, ауд. 222, корпус 1"
}
],
"4": [
{
"Время": "9:45 — 11:20",
"Неделя": null,
"Подгруппа 1": "—",
"Подгруппа 2": "Перевод научно-технической литературы нем. [лаб] (6.09—20.12) доц. Ладыжникова Татьяна Дмитриевна, ауд. 255"
},
{
"Время": "11:30 — 13:05",
"Неделя": "Н",
"Подгруппа 1": "—",
"Подгруппа 2": "—"
},
{
"Время": "13:30 — 15:05",
"Неделя": null,
"Подгруппа 1": "Вычислительная математика [лекц] (6.09—20.12) доц. Смирнова Татьяна Сергеевна, ауд. 270",
"Подгруппа 2": "Вычислительная математика [лекц] (6.09—20.12) доц. Смирнова Татьяна Сергеевна, ауд. 270"
},
{
"Время": "15:10 — 16:45",
"Неделя": "Н",
"Подгруппа 1": "—",
"Подгруппа 2": "Вычислительная математика [лаб] (6.09—13.12) доц. Смирнова Татьяна Сергеевна, ауд. 270"
},
{
"Время": "16:50 — 18:25",
"Неделя": "Н",
"Подгруппа 1": "—",
"Подгруппа 2": "Вычислительная математика [лаб] (6.09—13.12) доц. Смирнова Татьяна Сергеевна, ауд. 270"
}
],
"5": [
{
"Время": "9:45 — 11:20",
"Неделя": null,
"Подгруппа 1": "Дискретная математика [лекц] (7.09—21.12) доц. Смирнова Татьяна Сергеевна, ауд. 270",
"Подгруппа 2": "Дискретная математика [лекц] (7.09—21.12) доц. Смирнова Татьяна Сергеевна, ауд. 270"
},
{
"Время": "11:30 — 13:05",
"Неделя": "Н",
"Подгруппа 1": "—",
"Подгруппа 2": "Дискретная математика [лаб] (7.09—14.12) доц. Смирнова Татьяна Сергеевна, ауд. 270"
},
{
"Время": "13:30 — 15:05",
"Неделя": "Н",
"Подгруппа 1": "—",
"Подгруппа 2": "Дискретная математика [лаб] (7.09—14.12) доц. Смирнова Татьяна Сергеевна, ауд. 270"
}
],
"6": []
},
"В": {
"0": [
{
"Время": "9:45 — 11:20",
"Неделя": "В",
"Подгруппа 1": "Теоретические основы программирования [лаб] (9.09—16.12) доц. Кудрявцева Ирина Андреевна, ауд. 269",
"Подгруппа 2": "Архитектура ЭВМ [лаб] (21.10—16.12) доц. Матюшичев Илья Юрьевич, ауд. 267"
},
{
"Время": "11:30 — 13:05",
"Неделя": "В",
"Подгруппа 1": "Теоретические основы программирования [лаб] (9.09—16.12) доц. Кудрявцева Ирина Андреевна, ауд. 269",
"Подгруппа 2": "Архитектура ЭВМ [лаб] (21.10—16.12) доц. Матюшичев Илья Юрьевич, ауд. 267"
},
{
"Время": "13:30 — 15:05",
"Неделя": null,
"Подгруппа 1": "Риторика [лекц] (2.09—30.09) доц. Самойлова Елена Павловна, ауд. 335, корпус 1Архитектура ЭВМ [лекц] (21.10—16.12) доц. Матюшичев Илья Юрьевич, ауд. 365",
"Подгруппа 2": "Риторика [лекц] (2.09—30.09) доц. Самойлова Елена Павловна, ауд. 335, корпус 1Архитектура ЭВМ [лекц] (21.10—16.12) доц. Матюшичев Илья Юрьевич, ауд. 365"
},
{
"Время": "15:10 — 16:45",
"Неделя": null,
"Подгруппа 1": "Риторика [лекц] (2.09—23.09) доц. Самойлова Елена Павловна, ауд. 335, корпус 1Риторика [практ] (30.09) доц. Самойлова Елена Павловна, ауд. 335, корпус 1Архитектура ЭВМ [лаб] (21.10—23.12) доц. Матюшичев Илья Юрьевич, ауд. 267",
"Подгруппа 2": "Риторика [лекц] (2.09—23.09) доц. Самойлова Елена Павловна, ауд. 335, корпус 1Риторика [практ] (30.09) доц. Самойлова Елена Павловна, ауд. 335, корпус 1"
}
],
"1": [
{
"Время": "8:00 — 9:35",
"Неделя": "В",
"Подгруппа 1": "Физика [лаб] (10.09—22.10) проф. Серегин Павел Павлович, ауд. 366Физика [лаб] (5.11—17.12) проф. Серегин Павел Павлович, ауд. 367",
"Подгруппа 2": "—"
},
{
"Время": "9:45 — 11:20",
"Неделя": "В",
"Подгруппа 1": "Физика [лаб] (10.09—22.10) проф. Серегин Павел Павлович, ауд. 366Физика [лаб] (5.11—17.12) проф. Серегин Павел Павлович, ауд. 367",
"Подгруппа 2": "Теоретические основы программирования [лаб] (10.09—17.12) доц. Кудрявцева Ирина Андреевна, ауд. 269"
},
{
"Время": "11:30 — 13:05",
"Неделя": null,
"Подгруппа 1": "Физика [лекц] (3.09—17.12) проф. Серегин Павел Павлович, ауд. 237, корпус 1",
"Подгруппа 2": "Физика [лекц] (3.09—17.12) проф. Серегин Павел Павлович, ауд. 237, корпус 1"
},
{
"Время": "13:30 — 15:05",
"Неделя": null,
"Подгруппа 1": "Физическая культура и спорт (элективная дисциплина) [практ] (3.09—10.12, 9 студ.) доц. Фокин Александр Михайлович, ауд. 1, корпус 4",
"Подгруппа 2": "Физическая культура и спорт (элективная дисциплина) [практ] (3.09—10.12, 20 студ.) доц. Богданов Олег Андреевич, ауд. 1, корпус 4"
},
{
"Время": "15:10 — 16:45",
"Неделя": "В",
"Подгруппа 1": "—",
"Подгруппа 2": "Архитектура ЭВМ [лаб] (22.10—17.12) доц. Матюшичев Илья Юрьевич, ауд. 267"
}
],
"2": [],
"3": [
{
"Время": "9:45 — 11:20",
"Неделя": "В",
"Подгруппа 1": "Перевод научно-технической литературы исп. [лаб] (10.10—21.11, исп. яз.) доц. Овсянников Александр Олегович, ауд. 270",
"Подгруппа 2": "—"
},
{
"Время": "11:30 — 13:05",
"Неделя": null,
"Подгруппа 1": "Перевод научно-технической литературы [лаб] (5.09—19.12 англ.яз.) ст. преп. Хахалина Марина Сергеевна, ауд. 255",
"Подгруппа 2": "Перевод научно-технической литературы [лаб] (5.09—19.12, англ.язык) ст. преп. Порязь Надежда Вадимовна, ауд. 222, корпус 1Перевод научно-технической литературы фр. [лаб] (10.10 фр. яз., 24.10,фр.яз., 7.11, фр.яз., 21.11, фр.яз.) доц. Овсянников Александр Олегович, ауд. 270"
},
{
"Время": "13:30 — 15:05",
"Неделя": null,
"Подгруппа 1": "Физическая культура и спорт (элективная дисциплина) [практ] (5.09—5.12, 20 студ.) доц. Богданов Олег Андреевич, ауд. 1, корпус 4",
"Подгруппа 2": "Физическая культура и спорт (элективная дисциплина) [практ] (5.09—5.12, 9 студ.) доц. Фокин Александр Михайлович, ауд. 1, корпус 4Архитектура ЭВМ [лаб] (26.12) доц. Матюшичев Илья Юрьевич, ауд. 267"
},
{
"Время": "15:10 — 16:45",
"Неделя": null,
"Подгруппа 1": "Перевод научно-технической литературы исп. [лаб] (3.10—19.12, исп. язык) доц. Овсянников Александр Олегович, ауд. 222, корпус 1",
"Подгруппа 2": "Архитектура ЭВМ [лаб] (24.10—26.12) доц. Матюшичев Илья Юрьевич, ауд. 267"
},
{
"Время": "16:50 — 18:25",
"Неделя": null,
"Подгруппа 1": "—",
"Подгруппа 2": "Перевод научно-технической литературы фр. [лаб] (3.10—19.12 фр. яз.) доц. Овсянников Александр Олегович, ауд. 222, корпус 1"
}
],
"4": [
{
"Время": "9:45 — 11:20",
"Неделя": null,
"Подгруппа 1": "—",
"Подгруппа 2": "Перевод научно-технической литературы нем. [лаб] (6.09—20.12) доц. Ладыжникова Татьяна Дмитриевна, ауд. 255"
},
{
"Время": "11:30 — 13:05",
"Неделя": "В",
"Подгруппа 1": "Риторика [практ] (13.09—20.12) доц. Самойлова Елена Павловна, ауд. 314, корпус 1",
"Подгруппа 2": "Риторика [практ] (13.09—20.12) доц. Самойлова Елена Павловна, ауд. 314, корпус 1"
},
{
"Время": "13:30 — 15:05",
"Неделя": null,
"Подгруппа 1": "Вычислительная математика [лекц] (6.09—20.12) доц. Смирнова Татьяна Сергеевна, ауд. 270",
"Подгруппа 2": "Вычислительная математика [лекц] (6.09—20.12) доц. Смирнова Татьяна Сергеевна, ауд. 270"
},
{
"Время": "15:10 — 16:45",
"Неделя": "В",
"Подгруппа 1": "Вычислительная математика [лаб] (13.09—20.12) доц. Смирнова Татьяна Сергеевна, ауд. 270",
"Подгруппа 2": "—"
},
{
"Время": "16:50 — 18:25",
"Неделя": "В",
"Подгруппа 1": "Вычислительная математика [лаб] (13.09—20.12) доц. Смирнова Татьяна Сергеевна, ауд. 270",
"Подгруппа 2": "—"
}
],
"5": [
{
"Время": "9:45 — 11:20",
"Неделя": null,
"Подгруппа 1": "Дискретная математика [лекц] (7.09—21.12) доц. Смирнова Татьяна Сергеевна, ауд. 270",
"Подгруппа 2": "Дискретная математика [лекц] (7.09—21.12) доц. Смирнова Татьяна Сергеевна, ауд. 270"
},
{
"Время": "11:30 — 13:05",
"Неделя": "В",
"Подгруппа 1": "Дискретная математика [лаб] (14.09—21.12) доц. Смирнова Татьяна Сергеевна, ауд. 270",
"Подгруппа 2": "—"
},
{
"Время": "13:30 — 15:05",
"Неделя": "В",
"Подгруппа 1": "Дискретная математика [лаб] (14.09—21.12) доц. Смирнова Татьяна Сергеевна, ауд. 270",
"Подгруппа 2": "—"
}
],
"6": []
}
}
import base64
from collections import deque
import datetime
from itertools import chain
from os.path import exists
from urllib import parse
import simplejson as json
import pandas as pd
import requests
class _NetworkSchedule:
def __init__(self, url):
if "guide.herzen.spb.ru/static/schedule_view.php" not in url:
raise ValueError("URL invalid")
search_params = dict(parse.parse_qsl(parse.urlparse(url).query))
if not {"id_group", "sem"}.issubset(set(search_params)):
raise ValueError("URL invalid")
df = None
try:
r = requests.get(url)
dfs = pd.read_html(r.text)
df = dfs[0].iloc[:-2]
except (requests.ConnectionError, ValueError):
raise RuntimeError("Guide is dead")
all_weekdays = [
"понедельник",
"вторник",
"среда",
"четверг",
"пятница",
"суббота",
"воскресенье"
]
first_weekday_parsed_as_column_name = df.columns[0]
weekdays_df = df.loc[
df[first_weekday_parsed_as_column_name].isin(all_weekdays)
]
weekdays_list = \
[first_weekday_parsed_as_column_name] + weekdays_df.iloc[:, :1][
first_weekday_parsed_as_column_name
].tolist()
split_breakpoints_from_weekdays_list = [
0] + list(weekdays_df.index) + [None]
edu_groups = list(
map(
lambda n: f"Подгруппа {n}",
range(1, len(df.columns) - 2 + 1)
)
)
df.columns = ["Время", "Неделя"] + edu_groups
paired_breakpoints = list(
zip(
split_breakpoints_from_weekdays_list[:-1],
split_breakpoints_from_weekdays_list[1:]
)
)
slice_ranges = [paired_breakpoints[0]] + list(
map(
lambda t: (t[0] + 1, t[1]),
paired_breakpoints[1:]
)
)
split_df = list(
map(
lambda sr: df.iloc[sr[0]:sr[1]],
slice_ranges
)
)
missing_days = list(
map(
all_weekdays.index,
list(
set(weekdays_list) ^ set(all_weekdays)
)
)
)
deque(
map(
lambda missing_day_index:
split_df.insert(missing_day_index, pd.DataFrame()),
missing_days
),
0
)
self.split_df = split_df
with open(f"{search_params['id_group']}.json", mode="w") as f:
json.dump(
self.get_full_schedule(),
f,
ensure_ascii=False,
ignore_nan=True,
indent=2
)
def get_current_schedule(self):
display_day = \
datetime.datetime.now() + datetime.timedelta(days=1) \
if datetime.datetime.now().hour >= 18 else \
datetime.datetime.now()
week_type = "В" if int(display_day.strftime("%V")) & 1 else "Н"
selected_df = self.split_df[display_day.weekday()]
schedule = selected_df.loc[
(selected_df["Неделя"] == week_type) |
(selected_df["Неделя"].isnull())
] if not selected_df.empty else selected_df
return schedule.to_dict(orient="records")
def get_current_week_schedule(self):
week_type = \
"В" if int(datetime.datetime.now().strftime("%V")) & 1 else "Н"
def recursive_collection(pos):
if pos == 7:
return {}
selected_df = self.split_df[pos]
schedule = selected_df.loc[
(selected_df["Неделя"] == week_type) |
(selected_df["Неделя"].isnull())
] if not selected_df.empty else selected_df
return {
pos: schedule.to_dict(orient="records"),
**recursive_collection(pos + 1)
}
return recursive_collection(0)
def get_full_schedule(self):
def recursive_collection(pos):
if pos == 7:
return {"Н": {}, "В": {}}
selected_df = self.split_df[pos]
lower_week_schedule = selected_df.loc[
(selected_df["Неделя"] == "Н") |
(selected_df["Неделя"].isnull())
] if not selected_df.empty else selected_df
upper_week_schedule = selected_df.loc[
(selected_df["Неделя"] == "В") |
(selected_df["Неделя"].isnull())
] if not selected_df.empty else selected_df
return {
"Н": {
pos: lower_week_schedule.to_dict(orient="records"),
**recursive_collection(pos + 1)["Н"]
},
"В": {
pos: upper_week_schedule.to_dict(orient="records"),
**recursive_collection(pos + 1)["В"]
}
}
return recursive_collection(0)
class _LocalSchedule:
def __init__(self, url):
if "guide.herzen.spb.ru/static/schedule_view.php" not in url:
raise ValueError("URL invalid")
search_params = dict(parse.parse_qsl(parse.urlparse(url).query))
if not "id_group" in set(search_params):
raise ValueError("URL invalid")
if not exists(f"{search_params['id_group']}.json"):
raise FileNotFoundError("Local schedule data doesn't exist")
with open(f"{search_params['id_group']}.json", mode="r") as f:
self.data = json.load(f)
def get_current_schedule(self):
week_type = \
"В" if int(datetime.datetime.now().strftime("%V")) & 1 else "Н"
week_pos = datetime.datetime.now().weekday()
return self.data[week_type][week_pos]
def get_current_week_schedule(self):
week_type = \
"В" if int(datetime.datetime.now().strftime("%V")) & 1 else "Н"
return self.data[week_type]
def get_full_schedule(self):
return self.data
def perform_full_data_caching():
faculties_res = requests.post("https://know.herzen.spb.ru/api/v2/graphql", json={
"query": """
query{
getFaculties{
edges{
node{
id
name
fullName
fullNameLow
facility{
id
number
address
}
}
}
}
}
"""
})
faculties_json = faculties_res.json()
faculties_ids = map(
lambda obj:
base64.b64decode(obj["node"]["id"]).decode("utf-8").split(":")[1],
faculties_json["data"]["getFaculties"]["edges"]
)
def getFacultyGroupsData(facultyId):
groups_res = requests.post("https://know.herzen.spb.ru/api/v2/graphql", json={
"query": """
query($id: Int!){
findGroupsByFaculty(id: $id){
edges{
node{
id
fullName
degree{
name
}
plan{
kurs
studyForm{
id
name
}
}
}
}
}
}
""",
"variables": {"id": facultyId}
})
groups_json = groups_res.json()
groups_ids = map(
lambda obj:
base64.b64decode(obj["node"]["id"]).decode(
"utf-8").split(":")[1],
groups_json["data"]["findGroupsByFaculty"]["edges"]
)
today = datetime.datetime.now()
current_semester = None
if today.replace(day=1, month=9) <= today and today <= today.replace(day=31, month=12):
current_semester = 1
elif today.replace(day=1, month=1) <= today and today <= today.replace(day=31, month=8):
current_semester = 2
groups_semesters = map(
lambda obj:
(obj["node"]["plan"]["kurs"] - 1) * 2 + current_semester,
groups_json["data"]["findGroupsByFaculty"]["edges"]
)
return (groups_ids, groups_semesters)
def error_ignore(fn):
def wrap(*args, **kwargs):
try:
return fn(*args, **kwargs)
except RuntimeError:
pass
return wrap
deque(
map(
error_ignore(_NetworkSchedule),
chain.from_iterable(
map(
lambda groups_data_tuple: map(
lambda group_id, sem: f"https://guide.herzen.spb.ru/static/schedule_view.php?id_group={group_id}&sem={sem}",
groups_data_tuple[0],
groups_data_tuple[1]
),
map(
getFacultyGroupsData,
faculties_ids
)
)
)
),
0
)
class Schedule:
def __new__(cls, url):
try:
return _NetworkSchedule(url)
except RuntimeError:
return _LocalSchedule(url)
if __name__ == "__main__":
perform_full_data_caching()
html5lib==1.0.1
lxml==4.4.1
pandas==0.25.1
requests==2.22.0
simplejson==3.16.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment