All checks were successful
Build and Run VSTU Schedule Parser / build_and_run (push) Successful in 18s
243 lines
8.2 KiB
Python
243 lines
8.2 KiB
Python
# Copyright Stanislav Mironov
|
||
|
||
import time
|
||
import traceback
|
||
import xlrd
|
||
from coord import Coord
|
||
from translations import ExcelSheetReader
|
||
|
||
import hashlib
|
||
|
||
import requests
|
||
from urllib.parse import urlsplit, urlunsplit, quote
|
||
|
||
def get_preferer_facultet(facultets_data: dict, excel_url: str, skip_for=None, ):
|
||
if skip_for is None:
|
||
skip_for = []
|
||
|
||
for _key, _value in facultets_data.items():
|
||
if _key.startswith("_"):
|
||
continue
|
||
if _key in skip_for:
|
||
continue
|
||
|
||
short_names = _value.get("short_names", None)
|
||
if short_names is None:
|
||
continue
|
||
|
||
for name in short_names:
|
||
if name.lower() in excel_url.lower():
|
||
return _key
|
||
|
||
def get_abbrev_for_facultet(facultets_data: dict, facultet_id: str, fallback_not_found="?", fallback_error="?", fallback_no_short_name="?"):
|
||
if (facultet_id == 'mag'):
|
||
return "МАГ"
|
||
if (facultet_id == 'asp'):
|
||
return "АСП"
|
||
|
||
for _key, _value in facultets_data.items():
|
||
if _key != facultet_id:
|
||
continue
|
||
|
||
short_names = _value.get("short_names", None)
|
||
if short_names is None:
|
||
return fallback_no_short_name
|
||
|
||
try:
|
||
return short_names[0]
|
||
except Exception as e:
|
||
traceback.print_exception(e)
|
||
return fallback_error
|
||
return fallback_not_found
|
||
|
||
|
||
def download_file_from_url(url, output_filename):
|
||
"""
|
||
Скачивает файл по URL со спецсимволами и пробелами, сохраняя его под указанным именем.
|
||
|
||
Args:
|
||
url (str): Исходный URL, который может содержать пробелы и кириллицу.
|
||
output_filename (str): Имя файла для сохранения (например, 'calc.xls').
|
||
"""
|
||
try:
|
||
# --- Шаг 1: Правильное кодирование URL ---
|
||
# Разбираем URL на части: ('https', 'www.vstu.ru', '/path/to file.xls', '', '')
|
||
parts = urlsplit(url)
|
||
|
||
# Кодируем только путь, оставляя слэши '/' безопасными
|
||
# Это превратит ' ' в '%20', 'В' в '%D0%92' и т.д.
|
||
encoded_path = quote(parts.path, safe='/-_')
|
||
|
||
# Собираем URL обратно из частей с уже закодированным путем
|
||
encoded_url = urlunsplit((parts.scheme, parts.netloc, encoded_path, parts.query, parts.fragment))
|
||
|
||
|
||
# --- Шаг 2: Скачивание файла ---
|
||
response = requests.get(encoded_url, stream=True)
|
||
|
||
# Проверяем, успешен ли запрос (код 200 OK)
|
||
# Если сервер вернет ошибку (404, 500 и т.д.), здесь возникнет исключение
|
||
response.raise_for_status()
|
||
|
||
# --- Шаг 3: Сохранение файла ---
|
||
# Открываем файл для записи в бинарном режиме ('wb')
|
||
# Использование 'with' гарантирует, что файл будет закрыт автоматически
|
||
with open(output_filename, 'wb') as f:
|
||
for chunk in response.iter_content(chunk_size=8192):
|
||
f.write(chunk)
|
||
|
||
print(f"✅ Файл успешно скачан и сохранен как '{output_filename}'")
|
||
|
||
except requests.exceptions.RequestException as e:
|
||
print(f"❌ Ошибка скачивания: {e}")
|
||
|
||
except Exception as e:
|
||
print(f"❌ Произошла непредвиденная ошибка: {e}")
|
||
|
||
|
||
def calculate_sha1(filepath):
|
||
"""
|
||
Calculates the SHA1 hash of a given file.
|
||
|
||
Args:
|
||
filepath (str): The path to the file.
|
||
|
||
Returns:
|
||
str: The hexadecimal representation of the SHA1 hash, or None if the file is not found.
|
||
"""
|
||
sha1_hash = hashlib.sha1()
|
||
try:
|
||
with open(filepath, "rb") as f:
|
||
# Read the file in chunks to handle large files efficiently
|
||
for chunk in iter(lambda: f.read(4096), b""):
|
||
sha1_hash.update(chunk)
|
||
return sha1_hash.hexdigest()
|
||
except FileNotFoundError:
|
||
print(f"Error: File not found at {filepath}")
|
||
return None
|
||
except Exception as e:
|
||
print(f"An error occurred: {e}")
|
||
return None
|
||
|
||
class StepTimeCounter:
|
||
def __init__(self):
|
||
self.time: float = -1.0
|
||
self.createtime = time.time()
|
||
self.setnow()
|
||
|
||
def setnow(self):
|
||
self.time = time.time()
|
||
|
||
def step(self, no_set_now=False):
|
||
left = time.time() - self.time
|
||
if not no_set_now:
|
||
self.setnow()
|
||
return left
|
||
|
||
def from_create(self):
|
||
left = time.time() - self.createtime
|
||
return left
|
||
|
||
EMPTY_CTYPES = [xlrd.XL_CELL_EMPTY, xlrd.XL_CELL_BLANK]
|
||
|
||
def discards_list(trg, nones=True, emptystrings=True):
|
||
if nones: remove_from_list(trg, [None])
|
||
if emptystrings: remove_from_list(trg, [""])
|
||
|
||
def has_no_bottom_border(reader: "ExcelSheetReader", coord):
|
||
return reader.get_border_style(coord, 'bottom') == 0 and reader.get_border_style(coord.shift(down=1), 'top') == 0
|
||
|
||
def find_element_index(my_list, element):
|
||
if element in my_list:
|
||
return my_list.index(element)
|
||
else:
|
||
return -1
|
||
|
||
def next_element(arr, el):
|
||
index = find_element_index(arr, el)
|
||
return arr[index + 1]
|
||
|
||
def remove_from_list(l: list, todel: list):
|
||
for x in todel:
|
||
if x in l:
|
||
l.remove(x)
|
||
|
||
return l
|
||
|
||
def parse_all_dirt(reader: "ExcelSheetReader", min_pos: Coord, right, down, with_cells=False):
|
||
RET = set()
|
||
|
||
row = min_pos.row
|
||
while row < min_pos.row + down:
|
||
col = min_pos.col
|
||
while col < min_pos.col + right:
|
||
#print(excel_coordinate(row, col))
|
||
cv = reader.cell(row, col)
|
||
if cv is not None and not cv.is_empty():
|
||
RET.add(cv if with_cells else str(cv.value))
|
||
col += 1
|
||
row += 1
|
||
|
||
return RET
|
||
|
||
def excel_coordinate(row, col):
|
||
"""
|
||
Преобразует координаты строки и столбца (начиная с 0) в эквивалент Excel (например, A7, CB34).
|
||
|
||
Args:
|
||
row: Индекс строки (начиная с 0).
|
||
col: Индекс столбца (начиная с 0).
|
||
|
||
Returns:
|
||
Строка, представляющая координату ячейки в стиле Excel.
|
||
|
||
~ Google Gemini, tested
|
||
"""
|
||
|
||
col_str = ''
|
||
while col >= 0:
|
||
col_str = chr(ord('A') + col % 26) + col_str # Преобразуем в буквы, начиная с A
|
||
col = col // 26 - 1 # Уменьшаем номер столбца и учитываем переход к следующему разряду (как в 26-ричной системе)
|
||
|
||
return col_str + str(row + 1) # Добавляем номер строки (Excel начинается с 1)
|
||
|
||
def merged_humanize(crange):
|
||
"""Получить из 4 цифр границ AA:BB координаты как в Excel"""
|
||
row_low, col_low, row_high, col_high = crange # see order!
|
||
return excel_coordinate(row_low, col_low) + ":" + excel_coordinate(row_high, col_high)
|
||
|
||
def unspace(s: str):
|
||
"""Убрать пробелы из текста"""
|
||
if s is None:
|
||
return "!!!Python None!!!"
|
||
return s.strip().replace(" ", "").replace("\t", "")
|
||
|
||
def find(sh, query = None):
|
||
for rx in range(sh.nrows):
|
||
i = 0
|
||
for x in sh.row(rx):
|
||
if x.value == query:
|
||
return rx, i
|
||
i += 1
|
||
|
||
return None
|
||
|
||
def weekday_to_num(st: str):
|
||
if st.upper().strip().startswith("ПОНЕД"):
|
||
return 1
|
||
if st.upper().strip() == "ВТОРНИК":
|
||
return 2
|
||
if st.upper().strip() == "СРЕДА":
|
||
return 3
|
||
if st.upper().strip() == "ЧЕТВЕРГ":
|
||
return 4
|
||
if st.upper().strip() == "ПЯТНИЦА":
|
||
return 5
|
||
if st.upper().strip() == "СУББОТА":
|
||
return 6
|
||
if st.upper().strip().startswith("ВОСКР"):
|
||
return 7
|
||
|
||
print(f"Unknown weekday num for str: {st}; returnted -1")
|
||
return -1
|
||
|