diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,6 +1,6 @@ -from flask import Flask, render_template_string, request, redirect, url_for, session, send_file +from flask import Flask, render_template_string, request, redirect, url_for, session, send_from_directory import json import os import logging @@ -8,294 +8,566 @@ import threading import time from datetime import datetime from huggingface_hub import HfApi, hf_hub_download -from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError +from huggingface_hub.utils import RepositoryNotFoundError from werkzeug.utils import secure_filename -# from werkzeug.security import generate_password_hash, check_password_hash # Import if implementing password hashing app = Flask(__name__) -# IMPORTANT: Change this secret key to something truly random and secret! -app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'your_very_secret_unique_key_56789_cosmetics') +app.secret_key = 'your_unique_secret_key_12345' # Уникальный секретный ключ (Лучше сменить на более сложный) +DATA_FILE = 'data_soola.json' +USERS_FILE = 'users_soola.json' -DATA_FILE = 'data_cosmetics.json' -USERS_FILE = 'users_cosmetics.json' - -# Список файлов для синхронизации (config.json убран) +# Список файлов для синхронизации SYNC_FILES = [DATA_FILE, USERS_FILE] # Настройки Hugging Face -REPO_ID = "Kgshop/SoolaCosmetics" # Changed Repo ID +REPO_ID = "Kgshop/Soola" # Рекомендуется сменить на репозиторий для Soola Cosmetics HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") -# Адрес магазина (один) -SHOP_ADDRESS = "Рынок Дордой, Джунхай, терминал, 38" +# Адрес магазина +STORE_ADDRESS = "Рынок Дордой, Джунхай, терминал, 38" -# Настройка логирования -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +# Валюта (только сом) +CURRENCY_SYMBOL = 'с' +CURRENCY_CODE = 'KGS' -# --- Data Handling Functions --- +# Настройка логирования +logging.basicConfig(level=logging.DEBUG) -def download_db_from_hf(): - """Скачивает файлы базы данных (JSON) из Hugging Face.""" - if not HF_TOKEN_READ: - logging.warning("HF_TOKEN_READ не установлен. Пропуск скачивания с Hugging Face.") - return False # Indicate download was skipped or failed - - api = HfApi() - downloaded_any = False - for file_name in SYNC_FILES: +def load_data(): + """Загрузка данных о товарах и категориях.""" + try: + # Попытка скачать актуальные данные перед загрузкой try: - hf_hub_download( - repo_id=REPO_ID, - filename=file_name, - repo_type="dataset", - token=HF_TOKEN_READ, - local_dir=".", - local_dir_use_symlinks=False, - force_download=True, # Ensure we get the latest version - resume_download=False - ) - logging.info(f"Файл {file_name} успешно скачан из Hugging Face.") - downloaded_any = True - except RepositoryNotFoundError: - logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face. Проверьте REPO_ID и права доступа.") - # If repo not found, don't try other files - return False # Indicate failure - except EntryNotFoundError: - logging.warning(f"Файл {file_name} не найден в репозитории {REPO_ID}. Будет создан локально при первом сохранении, если не существует.") - # Continue trying to download other files + download_db_from_hf() except Exception as e: - logging.error(f"Ошибка при скачивании файла {file_name} из Hugging Face: {e}") - # Continue trying to download other files, but log the error - return downloaded_any # Return true if at least one file was potentially downloaded or existed + logging.warning(f"Не удалось скачать данные с Hugging Face при запуске: {e}. Используется локальная копия.") -def load_data(): - """Загружает данные о товарах и категориях.""" - if not os.path.exists(DATA_FILE): - logging.warning(f"Локальный файл {DATA_FILE} не найден. Попытка скачивания с Hugging Face...") - if not download_db_from_hf(): - logging.warning(f"Не удалось скачать {DATA_FILE} с Hugging Face. Создается пустая структура данных.") - return {'products': [], 'categories': []} - # Check again if download succeeded - if not os.path.exists(DATA_FILE): - logging.warning(f"{DATA_FILE} все еще не найден после попытки скачивания. Создается пустая структура данных.") - return {'products': [], 'categories': []} - - try: with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) - logging.info(f"Данные успешно загружены из {DATA_FILE}") - # Basic validation + logging.info("Данные успешно загружены из JSON") + # Проверка структуры данных if not isinstance(data, dict): - logging.warning(f"{DATA_FILE} не содержит словарь. Инициализация пустой структурой.") - return {'products': [], 'categories': []} - if 'products' not in data or not isinstance(data['products'], list): - logging.warning(f"Ключ 'products' отсутствует или не является списком в {DATA_FILE}. Инициализация.") + # Если старый формат (просто список продуктов), преобразуем + if isinstance(data, list): + return {'products': data, 'categories': []} + else: + return {'products': [], 'categories': []} + if 'products' not in data: data['products'] = [] - if 'categories' not in data or not isinstance(data['categories'], list): - logging.warning(f"Ключ 'categories' отсутствует или не является списком в {DATA_FILE}. Инициализация.") + if 'categories' not in data: data['categories'] = [] return data + except FileNotFoundError: + logging.warning(f"{DATA_FILE} не найден. Создание пустой структуры данных.") + return {'products': [], 'categories': []} except json.JSONDecodeError: - logging.error(f"Ошибка: Невозможно декодировать JSON файл {DATA_FILE}. Возвращается пустая структура.") + logging.error(f"Ошибка: Невозможно декодировать JSON файл {DATA_FILE}.") return {'products': [], 'categories': []} except Exception as e: - logging.error(f"Произошла ошибка при загрузке данных из {DATA_FILE}: {e}") + logging.error(f"Произошла непредвиденная ошибка при загрузке данных: {e}") return {'products': [], 'categories': []} + def save_data(data): - """Сохраняет данные о товарах и категориях в JSON и загружает на HF.""" + """Сохранение данных о товарах и категориях.""" try: with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) logging.info(f"Данные успешно сохранены в {DATA_FILE}") - upload_file_to_hf(DATA_FILE) # Upload the specific file + # Загружаем на HF после сохранения + upload_db_to_hf(DATA_FILE) except Exception as e: logging.error(f"Ошибка при сохранении данных в {DATA_FILE}: {e}") - # raise # Optionally re-raise if you want the caller to handle it + # Не прерываем работу приложения, но логируем ошибку + # raise # Можно раскомментировать, если критично прерывать работу при ошибке сохранения def load_users(): - """Загружает данные пользователей.""" - if not os.path.exists(USERS_FILE): - logging.warning(f"Локальный файл {USERS_FILE} не найден. Попытка скачивания с Hugging Face...") - if not download_db_from_hf(): - logging.warning(f"Не удалось скачать {USERS_FILE} с Hugging Face. Создается пустой словарь пользователей.") - return {} - # Check again if download succeeded - if not os.path.exists(USERS_FILE): - logging.warning(f"{USERS_FILE} все еще не найден после попытки скачивания. Создается пустой словарь пользователей.") - return {} - + """Загрузка данных пользователей.""" try: + # Попытка скачать актуальные данные перед загрузкой + try: + download_db_from_hf() + except Exception as e: + logging.warning(f"Не удалось скачать данные пользователей с Hugging Face при запуске: {e}. Используется локальная копия.") + with open(USERS_FILE, 'r', encoding='utf-8') as file: users = json.load(file) - if not isinstance(users, dict): - logging.warning(f"{USERS_FILE} не содержит словарь. Инициализация пустым словарем.") - return {} - logging.info(f"Данные пользователей успешно загружены из {USERS_FILE}") - return users + logging.info("Данные пользователей успешно загружены") + return users if isinstance(users, dict) else {} + except FileNotFoundError: + logging.warning(f"{USERS_FILE} не найден. Возвращен пустой словарь пользователей.") + return {} except json.JSONDecodeError: - logging.error(f"Ошибка: Невозможно декодировать JSON файл {USERS_FILE}. Возвращается пустой словарь.") + logging.error(f"Ошибка: Невозможно декодировать JSON файл {USERS_FILE}.") return {} except Exception as e: - logging.error(f"Произошла ошибка при загрузке пользователей из {USERS_FILE}: {e}") + logging.error(f"Произошла непредвиденная ошибка при загрузке пользователей: {e}") return {} def save_users(users): - """Сохраняет данные пользователей в JSON и загружает на HF.""" + """Сохранение данных пользователей.""" try: with open(USERS_FILE, 'w', encoding='utf-8') as file: json.dump(users, file, ensure_ascii=False, indent=4) logging.info(f"Данные пользователей успешно сохранены в {USERS_FILE}") - upload_file_to_hf(USERS_FILE) # Upload the specific file + # Загружаем на HF после сохранения + upload_db_to_hf(USERS_FILE) except Exception as e: - logging.error(f"Ошибка при сохранении пользователей в {USERS_FILE}: {e}") - # raise + logging.error(f"Ошибка при сохранении данных пользователей в {USERS_FILE}: {e}") -def upload_file_to_hf(file_name): - """Загружает указанный файл на Hugging Face.""" +def upload_db_to_hf(filename_to_upload=None): + """Загрузка файлов на Hugging Face. Если filename_to_upload не указан, загружает все файлы из SYNC_FILES.""" if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE не установлен. Пропуск загрузки на Hugging Face.") + logging.warning("HF_TOKEN (токен для записи) не установлен. Загрузка на Hugging Face отключена.") return + try: + api = HfApi() + files_to_sync = [filename_to_upload] if filename_to_upload else SYNC_FILES + + for file_name in files_to_sync: + if os.path.exists(file_name): + try: + api.upload_file( + path_or_fileobj=file_name, + path_in_repo=file_name, + repo_id=REPO_ID, + repo_type="dataset", + token=HF_TOKEN_WRITE, + commit_message=f"Автоматическое резервное копирование {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + logging.info(f"Резервная копия {file_name} успешно загружена на Hugging Face.") + except Exception as e: + logging.error(f"Ошибка при загрузке файла {file_name} на Hugging Face: {e}") + else: + logging.warning(f"Файл {file_name} не найден для загрузки на Hugging Face.") + except Exception as e: + logging.error(f"Общая ошибка при инициализации или загрузке на Hugging Face: {e}") - if not os.path.exists(file_name): - logging.warning(f"Файл {file_name} не найден локально. Пропуск загрузки на Hugging Face.") +def download_db_from_hf(): + """Скачивание файлов с Hugging Face.""" + if not HF_TOKEN_READ and not HF_TOKEN_WRITE: + logging.warning("HF_TOKEN_READ и HF_TOKEN (токен для чтения/записи) не установлены. Скачивание с Hugging Face отключено.") return + # Используем токен для записи, если токен для чтения не задан + token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE + try: - api = HfApi() - api.upload_file( - path_or_fileobj=file_name, - path_in_repo=file_name, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Обновлен файл {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - ) - logging.info(f"Файл {file_name} успешно загружен на Hugging Face ({REPO_ID}).") - except Exception as e: - logging.error(f"Ошибка при загрузке файла {file_name} на Hugging Face: {e}") + api = HfApi() # Api() не используется для скачивания, но можно оставить для единообразия + for file_name in SYNC_FILES: + try: + hf_hub_download( + repo_id=REPO_ID, + filename=file_name, + repo_type="dataset", + token=token_to_use, + local_dir=".", + local_dir_use_symlinks=False, # Важно для корректной перезаписи + force_download=True # Принудительно скачивать, чтобы получить свежую версию + ) + logging.info(f"Файл {file_name} успешно скачан из Hugging Face.") + except RepositoryNotFoundError: + logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face. Скачивание {file_name} невозможно.") + # Не прерываем скачивание остальных файлов + except Exception as e: # Ловим более конкретные ошибки по файлам + # Проверяем, является ли ошибка 'Not Found' (файл не существует в репо) + if "404 Client Error" in str(e) or "EntryNotFoundError" in str(e): + logging.warning(f"Файл {file_name} не найден в репозитории {REPO_ID}. Пропускаем скачивание.") + else: + logging.error(f"Ошибка при скачивании файла {file_name} из Hugging Face: {e}") + + except Exception as e: # Общая ошибка на уровне всего процесса скачивания + logging.error(f"Общая ошибка при скачивании файлов с Hugging Face: {e}") + # Не прерываем работу приложения, но логируем + # raise # Можно раскомментировать, если критично прервать работу при ошибке скачивания def periodic_backup(): - """Периодически загружает все SYNC_FILES на Hugging Face.""" + """Периодическая загрузка всех файлов на Hugging Face.""" while True: + time.sleep(800) # 13 минут 20 секунд logging.info("Запуск периодического резервного копирования...") - if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE не установлен. Пропуск периодического резервного копирования.") - time.sleep(1800) # Ждем 30 минут перед следующей проверкой - continue - - for file_name in SYNC_FILES: - upload_file_to_hf(file_name) - time.sleep(5) # Небольшая пауза между файлами - logging.info("Периодическое резервное копирование завершено.") - time.sleep(1800) # Интервал 30 минут (1800 секунд) - -# --- Flask Routes --- + upload_db_to_hf() # Загружаем все файлы @app.route('/') def catalog(): - # Попытка загрузить данные при каждом заходе на главную, чтобы подтянуть изменения - # download_db_from_hf() # Consider if this is too slow for every request data = load_data() products = data.get('products', []) categories = data.get('categories', []) is_authenticated = 'user' in session - # HTML шаблон каталога catalog_html = ''' - Soola Cosmetics - Каталог {/* Changed Title */} + Soola Cosmetics - Каталог @@ -304,22 +576,18 @@ def catalog():

Soola Cosmetics

+ - -
- Наш адрес: {{ shop_address }} -
- +
{{ store_address }}
{% for category in categories %} @@ -327,48 +595,49 @@ def catalog(): {% endfor %}
- +
- {% for product in products %} -
- {% if product.get('photos') and product['photos']|length > 0 %} -
- {{ product['name'] }} -
- {% else %} -
- {/* Placeholder icon */} + {% if products %} + {% for product in products %} +
+ {% if product.get('photos') and product['photos']|length > 0 %} +
+ {{ product['name'] }} +
+ {% else %} +
+ No Image Available +
+ {% endif %} +

{{ product['name'] }}

+ {% if is_authenticated %} +
{{ "%.2f"|format(product['price']) }} {{ currency_symbol }}
+ {% else %} +
Цена по запросу
+ {% endif %} +

{{ product['description'][:100] }}{% if product['description']|length > 100 %}...{% endif %}

+ + {% if is_authenticated %} + + {% endif %}
- {% endif %} -

{{ product['name'] }}

-

{{ product['description'] }}

{/* Show full desc, limited by CSS */} - {% if is_authenticated %} -
{{ product['price'] }} с
{/* Directly show price in KGS */} - {% else %} -
Цена по запросу
- {% endif %} - - {% if is_authenticated %} - - {% endif %} -
- {% endfor %} - {% if not products %} -

Товары не найдены или каталог пуст.

- {% endif %} + {% endfor %} + {% else %} +

Товары пока не добавлены.

+ {% endif %}
@@ -376,180 +645,219 @@ def catalog():