diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,5 +1,7 @@ -from flask import Flask, render_template_string, request, redirect, url_for, session, send_file +# --- START OF FILE Soola_Cosmetics.py --- + +from flask import Flask, render_template_string, request, redirect, url_for, session, send_from_directory import json import os import logging @@ -11,751 +13,433 @@ from huggingface_hub.utils import RepositoryNotFoundError from werkzeug.utils import secure_filename app = Flask(__name__) -app.secret_key = 'your_unique_secret_key_12345' # !!! CHANGE THIS TO A REAL SECRET KEY !!! -DATA_FILE = 'data_soola.json' -USERS_FILE = 'users_soola.json' +# Важно: Замените 'your_very_secure_secret_key_98765' на действительно случайный и секретный ключ +app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'your_very_secure_secret_key_98765') + +# --- Настройки Магазина --- +SHOP_NAME = "Soola Cosmetics" +SHOP_ADDRESS = "Рынок Дордой, Джунхай, терминал, 38" +PRIMARY_CURRENCY = 'KGS' # Основная и единственная валюта - сом +PRIMARY_CURRENCY_SYMBOL = 'с' # Символ сома -# Список файлов для синхронизации (config.json убран) -SYNC_FILES = [DATA_FILE, USERS_FILE] +# --- Файлы Данных и Конфигурации --- +DATA_FILE = 'data_soola_cosmetics.json' +USERS_FILE = 'users_soola_cosmetics.json' +# CONFIG_FILE убран, так как курс больше не нужен -# Настройки Hugging Face -REPO_ID = "Kgshop/Soola" # Or change to your new repo ID like "YourUsername/SoolaCosmetics" -HF_TOKEN_WRITE = os.getenv("HF_TOKEN") -HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") +# Список файлов для синхронизации с Hugging Face +SYNC_FILES = [DATA_FILE, USERS_FILE] # Убран CONFIG_FILE -# Адрес магазина (теперь один) -STORE_ADDRESS = "Рынок Дордой, Джунхай, терминал, 38" +# --- Настройки Hugging Face --- +# Убедитесь, что репозиторий соответствует вашему проекту +REPO_ID = "Kgshop/SoolaCosmetics" # Можно обновить имя репозитория, если хотите +HF_TOKEN_WRITE = os.getenv("HF_TOKEN") # Токен с правом записи +HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Токен с правом чтения (может быть тот же) -# Настройка логирования -logging.basicConfig(level=logging.INFO) # Changed to INFO for less noise, set to DEBUG if needed +# --- Настройка Логирования --- +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') -# --- Removed load_config, save_config, convert_price --- +# --- Функции Загрузки/Сохранения Данных --- def load_data(): - """Загрузка данных товаров и категорий.""" + """Загрузка данных о товарах и категориях.""" try: - # Attempt to download first to get the latest version - try: - download_db_from_hf(DATA_FILE) - except Exception as download_error: - logging.warning(f"Не удалось скачать {DATA_FILE} с HF, используется локальная версия (если есть): {download_error}") - - if not os.path.exists(DATA_FILE): - logging.warning(f"Локальный файл {DATA_FILE} не найден. Создание пустой структуры.") - return {'products': [], 'categories': []} - + # Попытка скачать актуальные данные перед чтением локального файла + download_db_from_hf() with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) logging.info(f"Данные успешно загружены из {DATA_FILE}") - if not isinstance(data, dict) or 'products' not in data or 'categories' not in data: - logging.warning(f"Структура файла {DATA_FILE} некорректна. Сброс к пустой структуре.") + # Проверка базовой структуры + if not isinstance(data, dict): + logging.warning(f"{DATA_FILE} имеет неверный формат (не словарь). Инициализация пустой структурой.") return {'products': [], 'categories': []} - # Ensure products and categories are lists - if not isinstance(data.get('products'), list): + if 'products' not in data: data['products'] = [] - if not isinstance(data.get('categories'), list): + if 'categories' not in data: data['categories'] = [] return data except FileNotFoundError: - logging.warning(f"Локальный файл {DATA_FILE} не найден. Создание пустой структуры.") - return {'products': [], 'categories': []} + logging.warning(f"Локальный файл {DATA_FILE} не найден. Попытка скачивания...") + try: + download_db_from_hf() + # Повторная попытка чтения после скачивания + with open(DATA_FILE, 'r', encoding='utf-8') as file: + data = json.load(file) + logging.info(f"Данные успешно загружены из {DATA_FILE} после скачивания.") + if not isinstance(data, dict): return {'products': [], 'categories': []} + if 'products' not in data: data['products'] = [] + if 'categories' not in data: data['categories'] = [] + return data + except FileNotFoundError: + logging.error(f"Файл {DATA_FILE} не найден даже после попытки скачивания. Создание пустой базы данных.") + return {'products': [], 'categories': []} + except RepositoryNotFoundError: + logging.error("Репозиторий Hugging Face не найден. Создание локальной пустой базы данных.") + return {'products': [], 'categories': []} + except Exception as e: + logging.error(f"Ошибка при загрузке данных после скачивания: {e}") + return {'products': [], 'categories': []} except json.JSONDecodeError: - logging.error(f"Ошибка: Невозможно декодировать JSON файл {DATA_FILE}.") - # Consider backing up the corrupted file here if needed + logging.error(f"Ошибка: Невозможно декодировать JSON из файла {DATA_FILE}. Возвращение пустых данных.") + # Попытка бэкапа испорченного файла + try: + corrupted_filename = f"{DATA_FILE}.corrupted_{datetime.now().strftime('%Y%m%d%H%M%S')}" + os.rename(DATA_FILE, corrupted_filename) + logging.info(f"Испорченный файл переименован в {corrupted_filename}") + except Exception as rename_e: + logging.error(f"Не удалось переименовать испорченный файл: {rename_e}") return {'products': [], 'categories': []} except Exception as e: - logging.error(f"Произошла ошибка при загрузке данных {DATA_FILE}: {e}") + logging.error(f"Непредвиденная ошибка при загрузке данных: {e}") return {'products': [], 'categories': []} def save_data(data): - """Сохранение данных товаров и категорий.""" + """Сохранение данных о товарах и категориях.""" 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_db_to_hf(DATA_FILE) # Upload specific file + # Запускаем загрузку на HF в отдельном потоке, чтобы не блокировать основной процесс + threading.Thread(target=upload_db_to_hf, args=([DATA_FILE],)).start() except Exception as e: logging.error(f"Ошибка при сохранении данных в {DATA_FILE}: {e}") - # Optionally re-raise or handle more gracefully - # raise + # Не пробрасываем исключение дальше, чтобы приложение продолжало работать, + # но ошибка залогирована. def load_users(): """Загрузка данных пользователей.""" try: - # Attempt to download first - try: - download_db_from_hf(USERS_FILE) - except Exception as download_error: - logging.warning(f"Не удалось скачать {USERS_FILE} с HF, используется локальная версия (если есть): {download_error}") - - if not os.path.exists(USERS_FILE): - logging.warning(f"Локальный файл {USERS_FILE} не найден. Создание пустого словаря.") - return {} - + # Попытка скачать актуальные данные перед чтением + download_db_from_hf([USERS_FILE]) # Скачиваем только файл пользователей with open(USERS_FILE, 'r', encoding='utf-8') as file: users = json.load(file) logging.info(f"Данные пользователей успешно загружены из {USERS_FILE}") - if not isinstance(users, dict): - logging.warning(f"Структура файла {USERS_FILE} некорректна. Сброс к пустому словарю.") - return {} - return users + return users if isinstance(users, dict) else {} except FileNotFoundError: - logging.warning(f"Локальный файл {USERS_FILE} не найден. Создание пустого словаря.") - return {} + logging.warning(f"Локальный файл {USERS_FILE} не найден. Попытка скачивания...") + try: + download_db_from_hf([USERS_FILE]) + with open(USERS_FILE, 'r', encoding='utf-8') as file: + users = json.load(file) + logging.info(f"Данные пользователей успешно загружены из {USERS_FILE} после скачивания.") + return users if isinstance(users, dict) else {} + except FileNotFoundError: + logging.error(f"Файл {USERS_FILE} не найден даже после скачивания. Создание пустого списка пользователей.") + return {} + except RepositoryNotFoundError: + logging.error("Репозиторий Hugging Face не найден при скачивании пользователей. Возвращение пустого списка.") + return {} + except Exception as e: + logging.error(f"Ошибка при загрузке пользователей после скачивания: {e}") + return {} except json.JSONDecodeError: - logging.error(f"Ошибка: Невозможно декодировать JSON файл {USERS_FILE}.") + logging.error(f"Ошибка декодирования JSON из {USERS_FILE}. Возвращение пустого списка.") + # Попытка бэкапа + try: + corrupted_filename = f"{USERS_FILE}.corrupted_{datetime.now().strftime('%Y%m%d%H%M%S')}" + os.rename(USERS_FILE, corrupted_filename) + logging.info(f"Испорченный файл пользователей переименован в {corrupted_filename}") + except Exception as rename_e: + logging.error(f"Не удалось переименовать испорченный файл пользователей: {rename_e}") return {} except Exception as e: - logging.error(f"Произошла ошибка при загрузке данных {USERS_FILE}: {e}") + logging.error(f"Непредвиденная ошибка при загрузке пользователей: {e}") return {} + def save_users(users): """Сохранение данных пользователей.""" 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_db_to_hf(USERS_FILE) # Upload specific file + # Запускаем загрузку на HF + threading.Thread(target=upload_db_to_hf, args=([USERS_FILE],)).start() except Exception as e: - logging.error(f"Ошибка при сохранении данных в {USERS_FILE}: {e}") - # Optionally re-raise or handle more gracefully - # raise + logging.error(f"Ошибка при сохранении пользователей в {USERS_FILE}: {e}") -def upload_db_to_hf(file_to_upload=None): - """Загрузка указанного файла или всех SYNC_FILES на Hugging Face.""" +# --- Функции Синхронизации с Hugging Face --- + +def upload_db_to_hf(files_to_sync=None): + """Загрузка указанных файлов (или всех SYNC_FILES) на Hugging Face.""" if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write) не установлен. Загрузка на Hugging Face отключена.") return + if files_to_sync is None: + files_to_sync = SYNC_FILES + try: api = HfApi() - files_to_process = [file_to_upload] if file_to_upload else SYNC_FILES - - for file_name in files_to_process: + logging.info(f"Попытка загрузки файлов: {files_to_sync} в репозиторий {REPO_ID}") + for file_name in files_to_sync: if os.path.exists(file_name): - logging.info(f"Попытка загрузки {file_name} на Hugging Face...") - 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.") + try: + api.upload_file( + path_or_fileobj=file_name, + path_in_repo=file_name, # Путь в репозитории совпадает с именем файла + repo_id=REPO_ID, + repo_type="dataset", # Репозитории для данных обычно типа dataset + token=HF_TOKEN_WRITE, + commit_message=f"Auto-sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + logging.info(f"Файл {file_name} успешно загружен на Hugging Face.") + except Exception as upload_exc: + logging.error(f"Ошибка при загрузке файла {file_name} на Hugging Face: {upload_exc}") else: - logging.warning(f"Файл {file_name} не найден для загрузки.") + logging.warning(f"Файл {file_name} не найден локально, пропуск загрузки.") except Exception as e: - logging.error(f"Ошибка при загрузке резервной копии на Hugging Face: {e}") + logging.error(f"Общая ошибка при инициализации или процессе загрузки на Hugging Face: {e}") + -def download_db_from_hf(file_to_download=None): - """Скачивание указанного файла или всех SYNC_FILES с Hugging Face.""" +def download_db_from_hf(files_to_sync=None): + """Скачивание указанных файлов (или всех SYNC_FILES) с Hugging Face.""" if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ не установлен. Скачивание с Hugging Face может быть недоступно для приватных репозиториев.") - # Allow public repo download attempt even without read token - # return # Uncomment this line if read token is strictly required + # Можно продолжить без токена для публичных репозиториев + # return # Раскомментируйте, если чтение без токена не нужно + + if files_to_sync is None: + files_to_sync = SYNC_FILES try: - api = HfApi() # Not strictly needed for download, but hf_hub_download uses underlying logic - files_to_process = [file_to_download] if file_to_download else SYNC_FILES - - for file_name in files_to_process: - logging.info(f"Попытка скачивания {file_name} из Hugging Face...") - hf_hub_download( - repo_id=REPO_ID, - filename=file_name, - repo_type="dataset", - token=HF_TOKEN_READ, # Pass None if not set, might work for public repos - local_dir=".", - local_dir_use_symlinks=False, - force_download=True # Ensure we get the latest version over local cache - ) - logging.info(f"Файл {file_name} успешно скачан из Hugging Face.") - except RepositoryNotFoundError as e: - logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face: {e}") - # Don't raise here, allow using local files if repo not found + api = HfApi() # Для скачивания токен не всегда обязателен, если репозиторий публичный + logging.info(f"Попытка скачивания файлов: {files_to_sync} из репозитория {REPO_ID}") + for file_name in files_to_sync: + 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 # Важно для избежания проблем с символическими ссылками + ) + logging.info(f"Файл {file_name} успешно скачан из Hugging Face.") + except RepositoryNotFoundError: + logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face.") + raise # Пробрасываем ошибку, чтобы load_data мог ее обработать + except Exception as download_exc: + # Логир��ем ошибку для конкретного файла, но пытаемся скачать остальные + logging.error(f"Ошибка при скачивании файла {file_name} из Hugging Face: {download_exc}") + # Если файла нет в репо, hf_hub_download вызовет исключение (например, EntryNotFoundError) + # Не пробрасываем его дальше, чтобы не прерывать скачивание других файлов + # Но если файл критичен (как DATA_FILE), load_data обработает FileNotFoundError except Exception as e: - # Catch other potential download errors (network issues, file not found in repo, etc.) - logging.error(f"Ошибка при скачивании файла {file_name if file_name else 'files'} с Hugging Face: {e}") - # Don't raise, let the app try to use local files + logging.error(f"Общая ошибка при инициализации или процессе скачивания с Hugging Face: {e}") + # Не пробрасываем общую ошибку, чтобы приложение могло запуститься с локальными данными, если они есть def periodic_backup(): """Периодическая загрузка всех файлов на Hugging Face.""" + logging.info("Запуск потока периодического резервного копирования.") while True: - time.sleep(800) # Wait first - logging.info("Запуск периодического резервного копирования...") - # Load current data before backup? Maybe not necessary, just upload existing files. - upload_db_to_hf() # Upload all sync files + time.sleep(800) # Пауза в 800 секунд (примерно 13 минут) + logging.info("Выполнение планового резервного копирования...") + upload_db_to_hf() # Загружаем все файлы из SYNC_FILES -# Initial load on startup -load_data() -load_users() +# --- Маршруты Flask --- @app.route('/') def catalog(): + """Главная страница каталога товаров.""" data = load_data() products = data.get('products', []) categories = data.get('categories', []) is_authenticated = 'user' in session + # Валюта теперь фиксирована + current_currency = PRIMARY_CURRENCY + current_currency_symbol = PRIMARY_CURRENCY_SYMBOL - catalog_html = ''' + catalog_html = f''' - Soola Cosmetics - Каталог + {SHOP_NAME} - Каталог
-

Soola Cosmetics

+

{SHOP_NAME}

-
- -
Наш адрес: {{ store_address }}
+ +
Наш адрес: {SHOP_ADDRESS}
- {% for category in categories %} + {{% for category in categories %}} - {% endfor %} + {{% endfor %}}
+
- +
+
- {% for product in products %} + {{% for product in products %}}
+ {{% if product.get('photos') and product['photos']|length > 0 %}}
- {% if product.get('photos') and product['photos']|length > 0 %} - {{ product['name'] }} - {% else %} - Нет изображения - {% endif %} + {{ product['name'] }} {# Обработка ошибки загрузки фото #}
-
-
-

{{ product['name'] }}

- {% if is_authenticated %} - -
{{ product['price'] }} KGS
- {% else %} -
Цена доступна после входа
- {% endif %} -

{{ product['description'] }}

-
-
- - {% if is_authenticated %} - - {% endif %} -
+ {{% else %}} +
Фото нет
+ {{% endif %}} +

{{ product['name'] }}

+ {{% if is_authenticated %}} +
{{ "%.2f"|format(product['price']) }} {current_currency_symbol}
{# Цена в сомах #} + {{% else %}} +
Цена доступна после входа
+ {{% endif %}} +

{{ product.get('description', 'Нет описания') }}

{# Убрано ограничение длины, CSS теперь обрезает #} +
{# Кнопки прижимаются к низу карточки #} + + {{% if is_authenticated %}} + + {{% endif %}}
- {% endfor %} + {{% endfor %}}
@@ -763,1501 +447,1308 @@ def catalog():