Обновления сентября 2025

This commit is contained in:
2025-10-08 10:24:08 +04:00
parent 4ef3e1d25f
commit 5022309848
7 changed files with 607 additions and 562 deletions
+406 -372
View File
@@ -1,373 +1,407 @@
import gspread
import requests
from datetime import datetime, timedelta
import json
import random
import time
import re
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)
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 = gspread.oauth()
self.document = gc.open_by_key("1srtrHkwbTCfvxyeDtb_ca4RMx8W1kxgUQ-MAIumamVA")
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 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] # Строка с названием лекции
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] # День недели
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:
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)