Files
ktalk-scheduler/structure.py
T

407 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import gspread
import requests
from datetime import datetime, timedelta
import json
import random
import time
import re
import os
from functools import lru_cache
@lru_cache(None)
def partialize(s):
s = s.lower().replace("ё", "е")
p = re.split("[. ]+", s)
return list(filter(lambda a: a, p))
def test_short(short: str, full: str) -> bool:
short_parts = partialize(short)
full_parts = partialize(full)
for i in range(min(len(short_parts), len(full_parts))):
if not full_parts[i].startswith(short_parts[i]):
return False
return True
def find_in_list(short: str, full_list: list) -> str|list:
result = []
for item in full_list:
if test_short(short, item):
result.append(item)
if len(result) == 1:
return result[0]
elif short in result:
return short
else:
return result
def find_teacher_by_date(input_string: str, target_date: str) -> str:
"""
Ищет преподавателя по целевой дате в строке с датами и ФИО.
:param input_string: Строка с датами и ФИО преподавателей.
:param target_date: Целевая дата в формате "%d.%m".
:return: ФИО преподавателя, связанного с целевой датой, или None, если дата не найдена.
"""
# Преобразуем целевую дату в формат "%d.%m"
target_date_str = target_date#.strftime("%d.%m")
# Регулярное выражение для поиска дат в формате "%d.%m"
date_pattern = re.compile(r"\d{2}\.\d{2}")
# Разделяем строку на строки по переносам
lines = re.split(" {5,}", input_string)
# Проходим по каждой строке
for i in range(len(lines)):
# Разделяем строку на даты и ФИО
parts = lines[i].strip().split()
# Извлекаем даты и ФИО
dates = []
teacher = ""
for part in parts:
if date_pattern.fullmatch(part.replace(",", "")): # Убираем запятые, если они есть
dates.append(part.replace(",", "")) # Убираем запятые, если они есть
else:
teacher += part + " " # Собираем ФИО
# Проверяем, есть ли целевая дата в списке дат
if target_date_str in dates:
return teacher.strip(), i # Возвращаем ФИО преподавателя
# Если дата не найдена
return input_string, 1
def shorten_name(name: str) -> str:
parts = name.split()
result = ""
for i in range(len(parts)):
if i == 0:
result = parts[0] + " "
else:
result += parts[i][0] + "."
return result
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class Lecture:
def __init__(self, auditorium: str, day_of_week: str, pair_number: str, lecture: str, teacher: str, regular: bool = True):
self.struct = Structure()
self.auditorium = auditorium
self.day_of_week = day_of_week
self.day_of_week_i = self.struct.settings["week"].index(day_of_week)
self.pair_number = pair_number
self.input_teacher = teacher
self.input_lecture = lecture
print("[Пара] ", self.auditorium, "/", self.day_of_week, "/", self.pair_number)
self.process_time()
if regular:
self.teacher = self.process_teacher()
if self.teacher == False:
self.complete = False
print()
return
self.lecture = self.process_lecture()
if self.lecture == False:
self.complete = False
print()
return
print("[Препод.] ", self.input_teacher, "->", self.teacher)
print("[Предмет] ", self.input_lecture, "->", self.lecture)
else:
self.teacher = self.input_teacher
self.lecture = self.input_lecture
self.name = self.format_name()
self.complete = True
print()
def process_time(self):
target_date = self.struct.monday + timedelta(days=self.day_of_week_i)
self.date = target_date.strftime("%d.%m")
for call in self.struct.calls:
if call["lesson"] == self.pair_number:
start_time = datetime.fromisoformat("2025-01-01T" + call["start_time"])
end_time = datetime.fromisoformat("2025-01-01T" + call["end_time"])
# Объединяем дату и время
self.start = target_date.replace(
hour=start_time.hour,
minute=start_time.minute,
second=start_time.second,
microsecond=0,
tzinfo=start_time.tzinfo
).isoformat(timespec="milliseconds")
self.end = target_date.replace(
hour=end_time.hour,
minute=end_time.minute,
second=end_time.second,
microsecond=0,
tzinfo=end_time.tzinfo
).isoformat(timespec="milliseconds")
def process_teacher(self) -> str:
teacher, self.teacher_index = find_teacher_by_date(self.input_teacher, self.date)
if teacher in self.struct.settings["aliases"].keys():
teacher = self.struct.settings["aliases"][teacher]
variants = find_in_list(teacher, self.struct.teachers)
return self.select_variant(teacher, variants, True)
def process_lecture(self) -> str:
# parts = re.split(" {5,}", self.input_lecture)
lecture, _ = find_teacher_by_date(self.input_lecture, self.date)
# if self.teacher_index < len(parts):
# lecture = parts[self.teacher_index]
# else:
# lecture = parts[0]
if lecture in self.struct.settings["aliases"].keys():
lecture = self.struct.settings["aliases"][lecture]
variants = find_in_list(lecture, self.struct.disciplines)
return self.select_variant(lecture, variants, False)
def format_name(self) -> str:
formatted_date = datetime.fromisoformat(self.start).strftime("%H:%M %d.%m.%Y")
return f"{self.lecture} [{shorten_name(self.teacher)}] ({formatted_date})"
def select_variant(self, search: str, variants: str|list, is_teacher: bool) -> bool|str:
if isinstance(variants, str):
return variants
print("Невозможно выбрать",
"преподавателя" if is_teacher else "лекцию",
"для значения",
f'"{search}".',
"Выберите вариант:" if len(variants) > 0 else "")
if len(variants) == 0:
return False
for i in range(len(variants)):
print(str(i + 1).rjust(3) + ".", variants[i])
print(" 0. [Пропустить]")
choice = int(input("Введите значение: ")) - 1
if choice < 0:
return False
return variants[choice]
def json(self) -> dict|None:
return {
"auditorium": self.auditorium,
"day_of_week": self.day_of_week,
"pair_number": self.pair_number,
"input_lecture": self.input_lecture,
"input_teacher": self.input_teacher,
"teacher": self.teacher,
"lecture": self.lecture,
"start": self.start,
"end": self.end,
"name": self.name
}
class Structure(metaclass=Singleton):
def __init__(self):
print("Подключение Google Sheets...", end=" ")
# gc = self.load_gspread()
# self.document = gc.open_by_key("1srtrHkwbTCfvxyeDtb_ca4RMx8W1kxgUQ-MAIumamVA")
self.document = self.load_gspread()
print("Готово.")
print("Загрузка настроек...", end=" ")
with open("settings.json", 'r', encoding='utf-8') as file:
self.settings = json.load(file)
print("Готово.")
print("Загрузка списка преподавателей...", end=" ")
self.teachers = list(map(lambda a: a["name"], self.get_json("teachers")))
print("Готово.")
print("Загрузка списка дисциплин...", end=" ")
self.disciplines = list(map(lambda a: a["name"], self.get_json("disciplines")))
print("Готово.")
print("Загрузка расписания...", end=" ")
self.calls = self.get_json("calls", "schedule")
print("Готово.")
self.monday = self.get_monday()
self.week = self.get_week_number()
print("Данные загружены!")
def load_gspread(self) -> gspread.Spreadsheet:
try:
gc = gspread.oauth()
except:
self.remove_auth_data()
gc = gspread.oauth()
try:
return gc.open_by_key("1srtrHkwbTCfvxyeDtb_ca4RMx8W1kxgUQ-MAIumamVA")
except:
self.remove_auth_data()
return gc.open_by_key("1srtrHkwbTCfvxyeDtb_ca4RMx8W1kxgUQ-MAIumamVA")
def remove_auth_data(self):
appdata_path = os.environ.get('APPDATA')
file_path = os.path.join(appdata_path, "gspread", "authorized_user.json")
os.remove(file_path)
def get_json(self, type, sub_type=None) -> dict:
resp = requests.get(url="https://ulsu.ru/oto/api/production/internal/getEnterpriseData.php?type=" + type, params={})
return resp.json()[type if sub_type is None else sub_type]
def get_monday(self):
today = datetime.today()
days_until_next_monday = (7 - today.weekday()) % 7
# Если сегодня понедельник, days_until_next_monday будет 0, поэтому добавляем 7 дней
# if days_until_next_monday == 0:
# days_until_next_monday = 7
return today + timedelta(days=days_until_next_monday)
def get_week_number(self, target_date:datetime|None=None):
if target_date is None:
target_date = self.monday
first_week_start_date = datetime.strptime(self.settings["first_week"], "%Y-%m-%d")
return ((target_date - first_week_start_date).days // 7) % 2 + 1
def get_worksheets(self):
return self.document.worksheets()
def get_document_data(self, worksheet, range_name=None, max_retries=5, initial_backoff=1, maximum_backoff=32):
retries = 0
while retries < max_retries:
try:
if range_name is None:
return worksheet.get_all_values()
else:
return worksheet.get(range_name)
except gspread.exceptions.APIError as e:
retries += 1
if retries == max_retries:
raise e
backoff = min(initial_backoff * (2 ** (retries - 1)) + random.random(), maximum_backoff)
time.sleep(backoff)
def find_range(self, worksheet, first_week=True):
def check(cell):
if first_week:
return "1 неделя" in cell or "I неделя" in cell
else:
return "2 неделя" in cell or "II неделя" in cell
all_values = self.get_document_data(worksheet)
week_row = None
for i, row in enumerate(all_values):
for j, cell in enumerate(row):
if check(cell):
week_row = i + 1
break
if week_row is not None:
break
for row in range(week_row, worksheet.row_count + 1):
cell_value = all_values[row][0]
if cell_value == "1":
return "C%d:H%d" % (row+1, row+16)
def get_lectures_from_range(self, worksheet, range_name) -> list:
# Получаем название аудитории из названия листа
auditorium = worksheet.title
# Получаем данные из указанного диапазона
data = self.get_document_data(worksheet, range_name)
if len(data) < 2:
return []
# Дни недели
days_of_week = self.settings["week"]
day_order = {day: idx for idx, day in enumerate(days_of_week)} # Словарь для порядка дней
# Список для хранения лекций
lectures = []
# Проходим по данным
for row_index in range(0, len(data), 2): # Шаг 2, так как каждая пара строк — это одна пара в расписании
lecture_row = data[row_index] # Строка с названием лекции
if row_index + 1 == len(data):
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
print(f"Ошибка с данными, row_index = {row_index + 1} не существует!")
print(data)
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
continue
teacher_row = data[row_index + 1] # Строка с преподавателем
# Номер пары
pair_number = (row_index // 2) + 1
# Проходим по дням недели (столбцам)
for col_index, (lecture, teacher) in enumerate(zip(lecture_row, teacher_row)):
if lecture.strip() and teacher.strip(): # Если есть и название лекции, и преподаватель
day_of_week = days_of_week[col_index] # День недели
# if col_index > 3:
# continue
lectures.append(Lecture(auditorium, day_of_week, pair_number, lecture, teacher))
lectures = sorted(lectures, key=lambda x: (x.day_of_week_i, x.pair_number))
return lectures
if __name__ == "__main__":
struct = Structure()
week = struct.get_week_number()
lectures = []
for worksheet in struct.get_worksheets():
print(worksheet.title + "...")
range_name = struct.find_range(worksheet, week == 1)
part = struct.get_lectures_from_range(worksheet, range_name)
lectures.extend(part)
lectures = list(filter(lambda l: l.complete, lectures))
with open("lecture.json", "w", encoding="utf-8") as f:
json.dump(list(map(lambda l: l.json(), lectures)), f, ensure_ascii=False, indent=4)