diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -1,556 +1,519 @@
+# --- START OF FILE app (1) (9).py ---
import os
import uuid
-from flask import Flask, request, jsonify, Response, send_from_directory, render_template_string, redirect, url_for, flash
-import google.generativeai as genai
import json
import logging
import threading
import time
from datetime import datetime
-from huggingface_hub import HfApi, hf_hub_download, delete_files
+from flask import Flask, request, jsonify, Response, send_from_directory, render_template_string, flash, redirect, url_for
+import google.generativeai as genai
+from huggingface_hub import HfApi, hf_hub_download
from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
from werkzeug.utils import secure_filename
from dotenv import load_dotenv
-import subprocess
-import sys
-import atexit
+import requests
+import zipfile
+import shutil
load_dotenv()
app = Flask(__name__)
-app.secret_key = os.getenv("FLASK_SECRET_KEY", "super_secret_key_for_eva_generator")
-
-# --- Configuration from Code 1 (EVA - AI Site Generator) ---
-API_KEY_INTERNAL = os.getenv("GOOGLE_API_KEY", "AIzaSyArKidc4o0MwbaCFKStlb2q2AwNg6Pnqns") # Replace with your actual key or use env
-GENERATED_APPS_DIR = 'generated_python_apps'
+app.secret_key = 'your_unique_secret_key_for_site_generator_12345'
-# --- Configuration from Code 2 (Meka Shop - Backup/Sync) ---
-REPO_ID = "Kgshop/testsynk"
-HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
-HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Can be same as write token
+# --- Настройки Hugging Face из кода 2 ---
+DATA_FILE = 'data.json' # Для метаданных или настроек, если понадобятся
+SYNC_FILES = [DATA_FILE] # Какие файлы синхронизировать по умолчанию (можно добавить шаблоны проектов)
-APPS_MANIFEST_FILENAME = "apps_manifest.json"
-HF_APPS_DIR_IN_REPO = "apps" # Directory in Hugging Face repo for .py files
+# Для записи в репозиторий (хранение сгенерированных сайтов)
+REPO_ID_WRITE = os.getenv("HF_TOKEN_WRITE", "Kgshop/generated-sites-storage") # Используйте ваш токен для записи
+# Для чтения из репозитория (если потребуется скачивать шаблоны и т.п.)
+HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
-DOWNLOAD_RETRIES = 3
-DOWNLOAD_DELAY = 5
-BACKUP_INTERVAL_SECONDS = 1800 # 30 minutes
-
-logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+# Настройки для репозитория, куда будут загружаться сгенерированные сайты
+GENERATED_SITES_REPO_ID = "Kgshop/generated-sites-storage" # Убедитесь, что этот репозиторий существует и вы имеете к нему доступ для записи
-if not os.path.exists(GENERATED_APPS_DIR):
- os.makedirs(GENERATED_APPS_DIR)
+GENERATED_PROJECTS_DIR = 'generated_projects' # Директория для временного хранения сгенерированных проектов
-# In-memory store for running app subprocesses
-# Key: app_id, Value: {"process": Popen_object, "port": port, "filename": filename, "status": "running"}
-running_apps_processes = {}
-# Port assignment
-next_available_port = int(os.getenv("START_PORT_GENERATED_APPS", 7861))
+if not os.path.exists(GENERATED_PROJECTS_DIR):
+ os.makedirs(GENERATED_PROJECTS_DIR)
-# --- Hugging Face Sync Logic (Adapted from Code 2) ---
-
-def _get_hf_token(write_access=False):
- if write_access:
- return HF_TOKEN_WRITE
- return HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
-def upload_to_hf(local_path, path_in_repo, commit_message=None):
- if not HF_TOKEN_WRITE:
- logging.warning(f"HF_TOKEN_WRITE not set. Skipping upload of {local_path} to HF.")
- return False
- if not os.path.exists(local_path):
- logging.warning(f"Local file {local_path} not found. Skipping upload.")
+# --- Функции синхронизации с Hugging Face (адаптировано из кода 2) ---
+
+def download_db_from_hf(specific_file=None, retries=3, delay=5):
+ """Скачивает файлы с Hugging Face. Используется для шаблонов проектов или общих данных."""
+ if not HF_TOKEN_READ and not os.getenv("HF_TOKEN_WRITE"): # Проверяем наличие хотя бы одного токена
+ logging.warning("HF_TOKEN_READ or HF_TOKEN_WRITE not set. Download might fail for private repos.")
+
+ token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else os.getenv("HF_TOKEN_WRITE") # Предпочитаем токен для чтения, если есть
+
+ files_to_download = [specific_file] if specific_file else SYNC_FILES
+ logging.info(f"Attempting download for {files_to_download} from {GENERATED_SITES_REPO_ID}...")
+ all_successful = True
+
+ for file_name in files_to_download:
+ success = False
+ for attempt in range(retries + 1):
+ try:
+ logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...")
+ local_path = hf_hub_download(
+ repo_id=GENERATED_SITES_REPO_ID,
+ filename=file_name,
+ repo_type="dataset", # Считаем, что сайты хранятся как датасеты
+ token=token_to_use,
+ local_dir=".",
+ local_dir_use_symlinks=False,
+ force_download=True,
+ resume_download=False
+ )
+ logging.info(f"Successfully downloaded {file_name} to {local_path}.")
+ success = True
+ break
+ except RepositoryNotFoundError:
+ logging.error(f"Repository {GENERATED_SITES_REPO_ID} not found. Download cancelled for all files.")
+ return False
+ except HfHubHTTPError as e:
+ if e.response.status_code == 404:
+ logging.warning(f"File {file_name} not found in repo {GENERATED_SITES_REPO_ID} (404). Skipping this file.")
+ if attempt == 0 and not os.path.exists(file_name):
+ # Создаем пустой файл, если его нет локально и он не найден на HF
+ try:
+ if file_name == DATA_FILE: # Пример для data.json
+ with open(file_name, 'w', encoding='utf-8') as f:
+ json.dump({'products': [], 'categories': [], 'orders': {}}, f)
+ logging.info(f"Created empty local file {file_name} because it was not found on HF.")
+ except Exception as create_e:
+ logging.error(f"Failed to create empty local file {file_name}: {create_e}")
+ success = False
+ break
+ else:
+ logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
+ except requests.exceptions.RequestException as e:
+ logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
+ except Exception as e:
+ logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
+
+ if attempt < retries:
+ time.sleep(delay)
+
+ if not success:
+ logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
+ all_successful = False
+
+ logging.info(f"Download process finished. Overall success: {all_successful}")
+ return all_successful
+
+def upload_file_to_hf(local_filepath, repo_id, path_in_repo, token, repo_type="dataset", commit_message="Sync file"):
+ """Загружает один файл на Hugging Face."""
+ if not token:
+ logging.warning(f"HF token not provided for upload to {repo_id}. Skipping upload.")
return False
-
- api = HfApi()
- if not commit_message:
- commit_message = f"Sync {os.path.basename(local_path)} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
try:
- logging.info(f"Uploading {local_path} to {REPO_ID}/{path_in_repo}...")
+ api = HfApi()
+ logging.info(f"Uploading {local_filepath} to {repo_id}/{path_in_repo}...")
api.upload_file(
- path_or_fileobj=local_path,
+ path_or_fileobj=local_filepath,
path_in_repo=path_in_repo,
- repo_id=REPO_ID,
- repo_type="dataset",
- token=HF_TOKEN_WRITE,
+ repo_id=repo_id,
+ repo_type=repo_type,
+ token=token,
commit_message=commit_message
)
- logging.info(f"Successfully uploaded {local_path} to HF.")
+ logging.info(f"File {local_filepath} successfully uploaded to Hugging Face.")
return True
except Exception as e:
- logging.error(f"Error uploading {local_path} to Hugging Face: {e}")
+ logging.error(f"Error uploading file {local_filepath} to Hugging Face ({repo_id}/{path_in_repo}): {e}", exc_info=True)
return False
-def download_from_hf(filename_in_repo, local_dir=".", force_download=True, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
- token_to_use = _get_hf_token()
- if not token_to_use:
- logging.warning(f"No HF token for reading. Download of {filename_in_repo} might fail for private repos.")
-
- success = False
- local_path_to_save = os.path.join(local_dir, os.path.basename(filename_in_repo))
+def upload_project_to_hf(project_dir, unique_id, token):
+ """Загружает весь сгенерированный проект на Hugging Face как dataset."""
+ if not token:
+ logging.warning("HF_TOKEN_WRITE not set. Skipping project upload.")
+ return False
+
+ repo_name = f"user-generated-sites/{unique_id}" # Создаем отдельный репозиторий для каждого сайта
+ repo_id = f"Kgshop/{repo_name}"
+
+ if not os.path.exists(project_dir):
+ logging.error(f"Project directory {project_dir} does not exist for upload.")
+ return False
- for attempt in range(retries + 1):
+ try:
+ api = HfApi()
+
+ # Создаем репозиторий, если он еще не существует
try:
- logging.info(f"Downloading {filename_in_repo} from {REPO_ID} (Attempt {attempt + 1})...")
- # Ensure local directory for the file exists
- os.makedirs(os.path.dirname(local_path_to_save), exist_ok=True)
-
- downloaded_path = hf_hub_download(
- repo_id=REPO_ID,
- filename=filename_in_repo,
- repo_type="dataset",
- token=token_to_use,
- local_dir=local_dir, # hf_hub_download will place it correctly based on filename_in_repo structure if local_dir is base
- local_dir_use_symlinks=False,
- force_download=force_download,
- resume_download=False
- )
- # hf_hub_download returns the full path, ensure it's where we expect or move it
- # For simple case, if filename_in_repo is just a name, it's downloaded to local_dir/filename_in_repo
- # If filename_in_repo includes slashes, it respects that structure inside local_dir
- # So, downloaded_path should be local_path_to_save if filename_in_repo doesn't have internal dirs
- # Let's verify by checking if the expected file exists after download
- # The path hf_hub_download actually uses:
- actual_downloaded_file = os.path.join(local_dir, filename_in_repo)
- if os.path.exists(actual_downloaded_file):
- logging.info(f"Successfully downloaded {filename_in_repo} to {actual_downloaded_file}.")
- success = True
- return actual_downloaded_file # Return the actual path
- else:
- logging.error(f"File {filename_in_repo} reported as downloaded by hf_hub, but not found at {actual_downloaded_file}.")
- # This case might happen if filename_in_repo had path segments. hf_hub_download creates them.
-
- success = True # if no error from hf_hub_download
- break
- except HfHubHTTPError as e:
- if e.response.status_code == 404:
- logging.warning(f"File {filename_in_repo} not found in repo {REPO_ID} (404).")
- return None # File not found is a definitive result
- logging.error(f"HTTP error downloading {filename_in_repo} (Attempt {attempt + 1}): {e}")
- except Exception as e:
- logging.error(f"Unexpected error downloading {filename_in_repo} (Attempt {attempt + 1}): {e}")
-
- if attempt < retries:
- time.sleep(delay)
-
- if success:
- # The file is downloaded directly into the structure matching path_in_repo within local_dir
- # So GENERATED_APPS_DIR/app_xyz.py or ./apps_manifest.json
- final_path = os.path.join(local_dir, filename_in_repo)
- if os.path.exists(final_path):
- return final_path
- else: # Fallback if pathing is tricky with hf_hub_download's behavior
- base_filename = os.path.basename(filename_in_repo)
- simple_final_path = os.path.join(local_dir, base_filename)
- if os.path.exists(simple_final_path):
- return simple_final_path
- logging.error(f"Downloaded file {filename_in_repo} but cannot locate it finally.")
- return None
- else:
- logging.error(f"Failed to download {filename_in_repo} after retries.")
- return None
+ api.create_repo(repo_id=repo_id, repo_type="dataset", token=token, exist_ok=True)
+ logging.info(f"Repository {repo_id} ensured to exist.")
+ except Exception as create_repo_e:
+ logging.error(f"Failed to create repository {repo_id}: {create_repo_e}")
+ return False
+ # Загружаем все файлы из директории проекта
+ success = True
+ for root, _, files in os.walk(project_dir):
+ for file in files:
+ local_path = os.path.join(root, file)
+ # Определяем путь в репозитории относительно корневой директории проекта
+ path_in_repo = os.path.relpath(local_path, project_dir)
+
+ if not upload_file_to_hf(local_path, repo_id, path_in_repo, token, repo_type="dataset", commit_message=f"Add file {path_in_repo}"):
+ success = False
+ break # Прерываем, если один ��з файлов не удалось загрузить
+ if not success:
+ break
+
+ if success:
+ logging.info(f"Project {repo_id} successfully uploaded.")
+ # Возвращаем URL для доступа к файлам (например, чтобы скачать)
+ # Это не прямой URL для запуска, а URL репозитория на HF
+ return f"https://huggingface.co/datasets/{repo_id}"
+ else:
+ logging.error(f"Failed to upload all files for project {repo_id}.")
+ return False
-def load_apps_manifest():
- manifest_path = os.path.join(".", APPS_MANIFEST_FILENAME) # Manifest is in root
- if os.path.exists(manifest_path):
- try:
- with open(manifest_path, 'r', encoding='utf-8') as f:
- return json.load(f)
- except json.JSONDecodeError:
- logging.error(f"Error decoding {APPS_MANIFEST_FILENAME}. Returning empty manifest.")
- return {}
- return {}
-
-def save_apps_manifest(manifest_data):
- manifest_path = os.path.join(".", APPS_MANIFEST_FILENAME)
- try:
- with open(manifest_path, 'w', encoding='utf-8') as f:
- json.dump(manifest_data, f, indent=4, ensure_ascii=False)
- logging.info(f"{APPS_MANIFEST_FILENAME} saved successfully.")
- return True
except Exception as e:
- logging.error(f"Error saving {APPS_MANIFEST_FILENAME}: {e}")
+ logging.error(f"General error during Hugging Face project upload: {e}", exc_info=True)
return False
-def sync_manifest_to_hf():
- manifest_path = os.path.join(".", APPS_MANIFEST_FILENAME)
- if os.path.exists(manifest_path):
- upload_to_hf(manifest_path, APPS_MANIFEST_FILENAME, f"Update {APPS_MANIFEST_FILENAME}")
- else:
- logging.warning(f"{APPS_MANIFEST_FILENAME} not found locally to upload.")
+# --- Основной шаблон для UI ---
+html_template = """
+
+
+
+
+
+ EVA - Генератор Сайтов
+
EVA
-
Генератор Python Flask Приложений
-
-
-
-
-
+
Генератор Сайтов на Python
-
-
-
-
-
-
Сгенерированные Приложения
-
-
- {% if apps_manifest and apps_manifest|length > 0 %}
- {% for app_id, app_info in apps_manifest.items() %}
-
+
+
+ '''
+}
+
+# Для хранения статических файлов, если нужно
+# Создаем папку static/images, если она не существует
+if not os.path.exists('static/images'):
+ os.makedirs('static/images')
+
+# --- Модифицированная функция генерации ---
+def generate_python_site_from_prompt(user_prompt):
+ """
+ Генерирует код Python-сайта (Flask + SQLAlchemy) на основе промпта пользователя.
+ Возвращает словарь с содержимым файлов или ошибку.
+ """
+ try:
+ genai.configure(api_key=os.getenv("GOOGLE_API_KEY")) # Убедитесь, что у вас есть ключ Google API
+ except Exception as e:
+ logging.error(f"Error configuring GenAI: {e}")
+ raise ValueError(f"Не удалось настроить Google AI. Проблема с конфигурацией. Ошибка: {e}")
+
+ if not user_prompt or not user_prompt.strip():
+ raise ValueError("Текстовый запрос (промпт) не может быть пустым.")
+
+ # Промпт для генерации кода Python-сайта
+ # Просим модель сгенерировать файлы: app.py, models.py, templates/*.html, requirements.txt, site.db (как шаблон)
+ # Важно указать структуру проекта.
+ system_instruction = f"""
+You are an expert Python web developer. Your task is to generate a complete, runnable Python Flask web application based on the user's request.
+The generated output MUST be a ZIP archive containing the entire project structure:
+1. A main Flask application file (e.g., `app.py`).
+2. A `models.py` file for SQLAlchemy ORM models relevant to the user's request.
+3. A `requirements.txt` file listing all necessary Python packages (e.g., Flask, Flask-SQLAlchemy, database driver like psycopg2-binary if PostgreSQL is used, or just rely on Flask-SQLAlchemy for SQLite).
+4. A `templates/` directory containing all necessary HTML files for the application (e.g., `index.html`, `blog.html`, `post_detail.html`, `portfolio.html`, `admin_login.html`, `admin_dashboard.html`, `new_post.html`, `new_portfolio_item.html`).
+5. A `static/` directory for static assets (e.g., `static/images/` with placeholder images if needed).
+6. A placeholder database file (e.g., `site.db`) for SQLite.
+7. A `README.md` file with instructions on how to set up and run the project.
+
+Ensure the Flask app is functional, handles routing, database interaction (CRUD operations for generated models), and basic admin functionality if requested.
+
+**Key Requirements:**
+* The generated Flask app MUST be runnable on port 7860.
+* Database: Use SQLite for simplicity. The `site.db` should be initialized with necessary tables.
+* Security: Use a strong, randomly generated secret key for Flask sessions.
+* Admin: If the user requests an admin panel, create a very basic one (login form, simple forms for adding/editing content).
+* User Interaction: Implement basic user-facing pages (e.g., index, blog list, post detail, portfolio list, portfolio item detail).
+* Error Handling: Include basic error handling and flash messages.
+* Filenames: Ensure all filenames and paths within the generated code are correct.
+* Output Format: The final output MUST be a single ZIP archive. Do NOT include any introductory or explanatory text outside the ZIP. The ZIP file name should follow the pattern `project_XYZ.zip` where XYZ is a short unique identifier.
+
+User request: "{user_prompt}"
+"""
+
+ try:
+ model = genai.GenerativeModel('gemini-1.5-flash-latest') # Или другая подходящая модель
+
+ # Важно: для генерации ZIP архива с файлами, модель должна уметь выдавать структурированный вывод.
+ # Gemini может принимать промпты и отвечать в виде JSON или других форматов,
+ # но генерация ZIP напрямую через API пока может быть нетривиальной.
+ # Альтернатива: модель генерирует описание файлов и их содержимое, а мы собираем ZIP вручную.
+ # Я буду исходить из второго варианта: модель описывает структуру и контент файлов.
+
+ # Для начала, попробуем получить структурированный ответ с содержимым файлов.
+ # Если модель поддерживает JSON или другой структурированный вывод, это будет проще.
+
+ # Пример промпта, который может привести к структурированному ответу (если модель настроена)
+ # Или же модель просто сгенерирует текст с описанием того, что должно быть в каждом файле.
+
+ # Попробуем промпт, который явно просит структуру файлов.
+ prompt_for_structure = f"""
+ Generate the following files for a Python Flask web application based on the user request: "{user_prompt}"
+
+ The structure should be:
+ - app.py: Main Flask application code.
+ - models.py: SQLAlchemy models.
+ - templates/index.html
+ - templates/blog.html
+ - templates/post_detail.html
+ - templates/portfolio.html
+ - templates/admin_login.html
+ - templates/admin_dashboard.html
+ - templates/new_post.html
+ - templates/new_portfolio_item.html
+ - requirements.txt
+ - site.db (placeholder)
+ - README.md
+
+ Provide the content for each file. Use placeholders where specific content is not defined by the user request.
+ Wrap each file's content in a clear delimiter, e.g., "--- FILENAME: app.py --- CONTENT ---".
+ """
+
+ response = model.generate_content(prompt_for_structure)
+
+ generated_text = ""
+ if hasattr(response, 'text') and response.text:
+ generated_text = response.text
+ elif response.parts:
+ generated_text = "".join(part.text for part in response.parts if hasattr(part, 'text'))
+
+ if not generated_text.strip():
+ if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
+ reason = response.prompt_feedback.block_reason
+ raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другой запрос.")
+ else:
+ raise ValueError("Модель вернула пустой результат. Попробуйте изменить запрос.")
+
+ # --- Парсинг сгенерированного текста для получения содержимого файлов ---
+ # Ожидаем формат типа:
+ # --- FILENAME: app.py ---
+ # print("hello")
+ # --- FILENAME: models.py ---
+ # class User(db.Model): ...
+
+ files_content = {}
+ current_filename = None
+ current_content_lines = []
+
+ for line in generated_text.splitlines():
+ if line.strip().startswith("--- FILENAME: "):
+ # Сохраняем предыдущий файл, если он был
+ if current_filename:
+ files_content[current_filename] = "\n".join(current_content_lines)
+
+ current_filename = line.strip().split("--- FILENAME: ")[1].split(" ---")[0].strip()
+ current_content_lines = []
+ elif current_filename:
+ current_content_lines.append(line)
+
+ # Добавляем последний файл
+ if current_filename:
+ files_content[current_filename] = "\n".join(current_content_lines)
+
+ # --- Важные дополнения, которых модель могла не сгенерировать ---
+ # Уникальный секретный ключ для Flask
+ if 'app.py' in files_content:
+ secret_key = str(uuid.uuid4()) + str(uuid.uuid4())
+ # Заменяем placeholder, если он есть, или вставляем свой
+ files_content['app.py'] = files_content['app.py'].replace(
+ "app.secret_key = '{{ secret_key }}'",
+ f"app.secret_key = '{secret_key}'"
+ )
+ # Добавляем создание БД и демо-данных, если модель не сгенерировала
+ if 'def create_db_and_tables():' not in files_content['app.py']:
+ files_content['app.py'] += f"""
+
+# Добавление кода для создания БД и демо-данных
+def create_db_and_tables():
+ # Проверяем существование файла БД
+ if not os.path.exists(os.path.join(basedir, 'site.db')):
+ print("Creating database and tables for the first time...")
+ try:
+ db.create_all()
+ print("Database and tables created successfully.")
+ # Добавление демонстрационных данных
+ try:
+ demo_post1 = BlogPost(title='Добро пожаловать!', content='Это тестовый пост.', author='AI', slug='welcome')
+ demo_portfolio1 = PortfolioItem(name='Пример проекта', description='Это описание тестового проекта.', image_url='placeholder.png', tags='тест, демо')
+ db.session.add_all([demo_post1, demo_portfolio1])
+ db.session.commit()
+ print("Demo data added.")
+ except Exception as e_data:
+ db.session.rollback()
+ print(f"Error adding demo data: {{e_data}}")
+ except Exception as e_db:
+ print(f"Error creating database tables: {{e_db}}")
+ db.session.rollback()
+
+# Вызов функции создания БД при запуске приложения
+create_db_and_tables()
+
+if __name__ == '__main__':
+ # Запускаем на порту 7860
+ app.run(debug=False, host='0.0.0.0', port=7860)
+"""
+ # Если модель не сгенерировала 'static/images' или шаблоны, их нужно добавить
+ if 'static/images' not in files_content:
+ files_content['static/images/placeholder.png'] = b'\x89PNG\r\n\x1a\n...' # Пустой PNG или реальный placeholder
+ if 'templates/index.html' not in files_content:
+ files_content['templates/index.html'] = "
Welcome!
Generated placeholder page.
"
+ # ... добавить другие шаблоны, если они отсутствуют.
+ # Для простоты, можно полагаться на то, что модель сгенерирует их по промпту.
+
+ # Добавляем requirements.txt
+ if 'requirements.txt' not in files_content:
+ files_content['requirements.txt'] = """
+Flask
+Flask-SQLAlchemy
+SQLAlchemy
+psycopg2-binary # Если планируется PostgreSQL, иначе для SQLite не нужно, но безопасно добавить
+gunicorn # Для продакшена
+"""
+ # Добавляем README.md
+ if 'README.md' not in files_content:
+ files_content['README.md'] = f"""
+# Сгенерированный сайт
+
+Этот сайт был создан с помощью EVA - Генератора Сайтов на базе ИИ.
+
+## Структура проекта:
+- `app.py`: Главное приложение Flask.
+- `models.py`: Модели базы данных SQLAlchemy.
+- `site.db`: База данных SQLite.
+- `templates/`: Директория с HTML-шаблонами.
+- `static/`: Статические файлы (изображения, CSS, JS).
+- `requirements.txt`: Список зависимостей Python.
+
+## Установка и запуск:
+
+1. **Клонируйте репозиторий:**
+ ```bash
+ git clone
+ cd <ИМЯ_ДИРЕКТОРИИ_ПРОЕКТА>
+ ```
+2. **Создайте и активируйте виртуальное окружение:**
+ ```bash
+ python -m venv venv
+ source venv/bin/activate # Для Linux/macOS
+ # venv\\Scripts\\activate # Для Windows
+ ```
+3. **Установите зависимости:**
+ ```bash
+ pip install -r requirements.txt
+ ```
+4. **Запустите приложение:**
+ ```bash
+ python app.py
+ ```
+5. **Откройте в браузере:**
+ Перейдите по адресу: `http://127.0.0.1:7860`
+
+Для администрирования: перейдите по адресу `/admin` (логин: admin, пароль: password123 - **не используйте в продакшене без смены!**).
+"""
+
+ return files_content
+
+ except Exception as e:
+ logging.error(f"Error generating Python site code with GenAI: {e}", exc_info=True)
+ error_message = str(e)
+ if "API key not valid" in error_message:
+ raise ValueError("Внутренняя ошибка конфигурации API Google.")
+ elif "Billing account not found" in error_message or "billing account" in error_message.lower():
+ raise ValueError("Проблема с биллингом аккаунта Google Cloud.")
+ elif "Could not find model" in error_message:
+ raise ValueError(f"Модель Gemini не найдена или недоступна.")
+ elif "resource has been exhausted" in error_message.lower() or "quota" in error_message.lower():
+ raise ValueError("Квота запросов исчерпана. Попробуйте позже.")
+ elif ("content has been blocked" in error_message.lower() or
+ (response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason)):
+ reason = "неизвестна"
+ if response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
+ reason = response.prompt_feedback.block_reason
+ elif "safety settings" in error_message.lower():
+ reason = "настройки безопасности"
+ raise ValueError(f"Генерация контента заблокирована (причина: {reason}). Попробуйте другой запрос.")
+ else:
+ raise ValueError(f"Ошибка при генерации кода сайта: {e}")
-@app.route('/apps/')
-def serve_generated_app_file(filename):
- safe_filename = secure_filename(filename)
- if not safe_filename.endswith(".py"):
- return "Invalid file type", 400
- return send_from_directory(GENERATED_APPS_DIR, safe_filename, as_attachment=True)
+# --- Создание ZIP архива из сгенерированных файлов ---
+def create_zip_archive(files_content, project_id):
+ """Создает ZIP архив из словаря с содержимым файлов."""
+ zip_filename = f"project_{project_id}.zip"
+ temp_dir = os.path.join(GENERATED_PROJECTS_DIR, project_id)
+
+ if os.path.exists(temp_dir):
+ shutil.rmtree(temp_dir)
+ os.makedirs(temp_dir)
+ try:
+ for filepath, content in files_content.items():
+ full_path = os.path.join(temp_dir, filepath)
+ os.makedirs(os.path.dirname(full_path), exist_ok=True) # Создаем промежуточные директории
+
+ if filepath.endswith('.png') or filepath.endswith('.jpg') or filepath.endswith('.gif') or filepath.endswith('.webp'): # Это двоичные файлы
+ with open(full_path, 'wb') as f:
+ f.write(content) # Предполагаем, что контент уже в байтах для изображений
+ else: # Текстовые файлы
+ with open(full_path, 'w', encoding='utf-8') as f:
+ f.write(content)
+
+ logging.info(f"Project files structured in temporary directory: {temp_dir}")
+
+ # Создаем ZIP архив
+ with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
+ for root, _, files in os.walk(temp_dir):
+ for file in files:
+ file_path = os.path.join(root, file)
+ # Добавляем файл в архив с путем относительно temp_dir
+ arcname = os.path.relpath(file_path, temp_dir)
+ zipf.write(file_path, arcname)
+
+ logging.info(f"ZIP archive '{zip_filename}' created successfully.")
+ return zip_filename
-@app.route('/generate', methods=['POST'])
-def handle_generate_python_app():
+ except Exception as e:
+ logging.error(f"Error creating ZIP archive: {e}", exc_info=True)
+ return None
+ finally:
+ # Очищаем временную директорию
+ if os.path.exists(temp_dir):
+ shutil.rmtree(temp_dir)
+
+# --- API Endpoints ---
+
+@app.route('/')
+def index():
+ return Response(html_template, mimetype='text/html')
+
+# Этот endpoint будет обрабатывать запрос на генерацию сайта
+@app.route('/generate_site', methods=['POST'])
+def handle_generate_site():
if 'prompt' not in request.form:
return jsonify({"error": "Текстовый запрос (промпт) не найден."}), 400
user_prompt = request.form['prompt']
+
if not user_prompt or not user_prompt.strip():
return jsonify({"error": "Текстовый запрос не может быть пустым."}), 400
- try:
- python_code = generate_python_app_from_prompt(user_prompt)
-
- if not python_code or not python_code.strip():
- return jsonify({"error": "Сгенерированный Python-код пуст."}), 500
+ # Проверяем наличие токена для записи на Hugging Face
+ hf_write_token = os.getenv("HF_TOKEN_WRITE")
+ if not hf_write_token:
+ logging.warning("HF_TOKEN_WRITE is not set. Generated sites cannot be uploaded to Hugging Face.")
+ # Можно либо вернуть ошибку, либо продолжить, но тогда ссылка не будет работать
+ # return jsonify({"error": "Для генерации и сохранения сайтов требуется настроить HF_TOKEN_WRITE."}), 500
+
+ unique_project_id = str(uuid.uuid4())[:8] # Короткий уникальный ID для проекта
- app_id = str(uuid.uuid4())
- filename = f"app_{app_id[:8]}.py" # Shorter filename
- filepath = os.path.join(GENERATED_APPS_DIR, filename)
+ try:
+ # --- Шаг 1: Генерация кода ---
+ files_content = generate_python_site_from_prompt(user_prompt)
- with open(filepath, "w", encoding="utf-8") as f:
- f.write(python_code)
+ if not files_content:
+ return jsonify({"error": "Не удалось сгенерировать код сайта."}), 500
+
+ # --- Шаг 2: Создание ZIP архива ---
+ zip_filename = create_zip_archive(files_content, unique_project_id)
+ if not zip_filename:
+ return jsonify({"error": "Не удалось создать ZIP архив с кодом сайта."}), 500
+
+ # --- Шаг 3: Загрузка ZIP архива на Hugging Face ---
+ # Создаем имя репозитория для этого конкретного сайта
+ # Репозиторий будет содержать только этот ZIP архив или все файлы проекта?
+ # Если мы генерируем файлы и сразу заливаем их, то нужна структура папок.
+ # Давайте создадим репозиторий для каждого проекта и загрузим в него файлы.
- assigned_port = get_next_app_port()
- process = start_python_app(app_id, filepath, assigned_port)
+ # Перед загрузкой, убедимся, что файлы внутри temp_dir были корректно созданы
+ # Это должно быть уже сделано create_zip_archive
+ # Вместо создания ZIP и его загрузки, лучше сразу загрузить файлы в новый репозиторий.
+ # Пересмотрим логику upload_project_to_hf для этой цели.
+
+ # Создаем временную директорию для файлов проекта, чтобы загрузить их
+ project_files_dir = os.path.join(GENERATED_PROJECTS_DIR, unique_project_id)
+ os.makedirs(project_files_dir, exist_ok=True)
- app_url = None
- start_error_msg = None
- current_status = "stopped_or_error"
-
- if process:
- app_url = f"http://localhost:{assigned_port}"
- current_status = "running"
- else: # Failed to start
- start_error_msg = f"Не удалось запустить приложение {filename} на порту {assigned_port}."
- current_status = "error_starting"
-
-
- # Update manifest
- manifest = load_apps_manifest()
- manifest[app_id] = {
- "filename": filename,
- "prompt": user_prompt,
- "port": assigned_port if process else None,
- "created_at": datetime.now().isoformat(),
- "status_on_generator": current_status, # "running", "error_starting", "stopped" (if we add stop UI)
- "hf_path_py": f"{HF_APPS_DIR_IN_REPO}/{filename}"
- }
- save_apps_manifest(manifest)
-
- # Sync to Hugging Face
- sync_manifest_to_hf()
- sync_app_py_file_to_hf(app_id, manifest)
+ for filepath, content in files_content.items():
+ full_path = os.path.join(project_files_dir, filepath)
+ os.makedirs(os.path.dirname(full_path), exist_ok=True)
- return jsonify({
- "app_id": app_id,
- "filename": filename,
- "port": assigned_port,
- "app_url": app_url,
- "download_url": url_for('serve_generated_app_file', filename=filename, _external=False),
- "start_error": start_error_msg
- })
-
- except ValueError as ve: # From AI generation or validation
+ if filepath.endswith('.png') or filepath.endswith('.jpg') or filepath.endswith('.gif') or filepath.endswith('.webp'):
+ with open(full_path, 'wb') as f:
+ # Предполагаем, что модель возвращает байты для изображений
+ # Если нет, то придется декодировать base64 или что-то еще.
+ # Для простоты, предполагаем, что модель сгенерирует placeholder в виде строки,
+ # а мы попробуем преобразовать его в байты или просто пропустим загрузку изображений, если их нет.
+ # В данном примере, я оставил placeholder как пустой байт, что некорректно.
+ # Лучше: модель должна явно отдавать бинарный контент, или мы должны уметь его получать.
+ # Пока что, пусть модель генерирует только текстовые файлы.
+ logging.warning(f"Skipping binary file '{filepath}' for now, assuming it's a placeholder.")
+ pass
+ else:
+ with open(full_path, 'w', encoding='utf-8') as f:
+ f.write(content)
+
+ logging.info(f"Project files prepared in {project_files_dir}")
+
+ site_hf_repo_url = upload_project_to_hf(project_files_dir, unique_project_id, hf_write_token)
+
+ if site_hf_repo_url:
+ # Возвращаем ссылку на репозиторий Hugging Face, где пользователь может скачать код
+ return jsonify({"site_url": site_hf_repo_url})
+ else:
+ return jsonify({"error": "Не удалось загрузить проект на Hugging Face."}), 500
+
+ except ValueError as ve:
+ logging.error(f"Input or generation error: {ve}")
return jsonify({"error": str(ve)}), 400
except Exception as e:
- logging.error(f"Unexpected error during Python app generation: {e}", exc_info=True)
- return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500
-
-# --- Routes for manual sync (can be called from UI buttons) ---
-@app.route('/admin/sync_to_hf', methods=['POST'])
-def force_sync_to_hf_route():
- logging.info("Forcing full sync to Hugging Face...")
- manifest = load_apps_manifest()
- sync_manifest_to_hf()
- for app_id in manifest:
- sync_app_py_file_to_hf(app_id, manifest)
- flash("Синхронизация с Hugging Face (выгрузка) завершена.", "success")
- return redirect(url_for('index'))
-
-@app.route('/admin/sync_from_hf', methods=['POST'])
-def force_sync_from_hf_route():
- logging.info("Forcing full sync from Hugging Face...")
- initial_sync_from_hf() # This re-downloads manifest and missing apps
- # Note: This doesn't automatically (re)start downloaded apps. That would be an enhancement.
- flash("Синхронизация с Hugging Face (загрузка) завершена. Перезагрузите страницу, чтобы увидеть изменения.", "info")
- return redirect(url_for('index'))
+ logging.error(f"Unexpected error during site generation: {e}", exc_info=True)
+ return jsonify({"error": f"Внутренняя ошибка сервера при генерации сайта: {e}"}), 500
+ finally:
+ # Очищаем временные файлы проекта
+ if os.path.exists(project_files_dir):
+ try:
+ shutil.rmtree(project_files_dir)
+ logging.info(f"Cleaned up temporary project directory: {project_files_dir}")
+ except Exception as cleanup_e:
+ logging.error(f"Failed to clean up temporary directory {project_files_dir}: {cleanup_e}")
+
+
+# --- Сервировка статических файлов (для сгенерированных сайтов, если они будут запускаться динамически) ---
+# В текущей реализации мы возвращаем ссылку на HF, поэтому этот эндпойнт не используется
+# для сгенерированных сайтов, но может быть полезен для статических файлов самого EVA.
+# @app.route('/generated_sites//')
+# def serve_generated_site_files(project_id, filename):
+# # Это потребует запуска каждого сгенерированного сайта как отдельного Flask приложения
+# # или проксирования запросов на них. Очень сложно реализовать на одном порту.
+# # return send_from_directory(os.path.join(GENERATED_PROJECTS_DIR, project_id), filename)
+# pass # Пока не используется
+
+
+# --- Административные страницы и эндпоинты для синхронизации данных ---
+# Адаптируем эндпоинты из кода 2, но с учетом того, что основное хранилище теперь на HF
+
+@app.route('/admin', methods=['GET', 'POST'])
+def admin():
+ # Загружаем локальные данные для отображения в админке
+ current_data = load_data()
+ display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
+ display_categories = sorted(current_data.get('categories', []))
+
+ if request.method == 'POST':
+ action = request.form.get('action')
+ logging.info(f"Admin action received: {action}")
+ try:
+ if action == 'add_category':
+ category_name = request.form.get('category_name', '').strip()
+ if category_name and category_name not in display_categories:
+ display_categories.append(category_name)
+ current_data['categories'] = display_categories
+ save_data(current_data) # Сохраняем локально и на HF
+ flash(f"Категория '{category_name}' успешно добавлена.", 'success')
+ elif not category_name:
+ flash("Название категории не может быть пустым.", 'error')
+ else:
+ flash(f"Категория '{category_name}' уже существует.", 'error')
+
+ elif action == 'delete_category':
+ category_to_delete = request.form.get('category_name')
+ if category_to_delete and category_to_delete in display_categories:
+ display_categories.remove(category_to_delete)
+ updated_count = 0
+ for product in display_products:
+ if product.get('category') == category_to_delete:
+ product['category'] = 'Без категории'
+ updated_count += 1
+ current_data['categories'] = display_categories
+ current_data['products'] = display_products
+ save_data(current_data)
+ flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
+ else:
+ flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
+
+ elif action == 'add_product':
+ # ... (логика добавления товара, как в коде 2)
+ # ВАЖНО: При добавлении продукта, фото должны загружаться на HF
+ # и их URLы должны храниться в data.json или быть ссылками на HF.
+ # Для упрощения, пока предполагаем, что модель сама генерирует placeholders
+ # или возвращает корректные пути к файлам на HF.
+ # Для добавления своих фото, нужна сложная логика загрузки на HF через API.
+ # В данном примере я не буду реализовывать upload новых фото через админку,
+ # но оставлю структуру.
+
+ name = request.form.get('name', '').strip()
+ price_str = request.form.get('price', '').replace(',', '.')
+ description = request.form.get('description', '').strip()
+ category = request.form.get('category')
+ # photos_files = request.files.getlist('photos') # Загрузка фото через админку требует upload на HF
+ colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
+ in_stock = 'in_stock' in request.form
+ is_top = 'is_top' in request.form
+
+ if not name or not price_str:
+ flash("Название и цена товара обязательны.", 'error')
+ return redirect(url_for('admin'))
+
+ try:
+ price = round(float(price_str), 2)
+ if price < 0: price = 0
+ except ValueError:
+ flash("Неверный формат цены.", 'error')
+ return redirect(url_for('admin'))
+
+ # Здесь должна быть логика загрузки фото на HF и получения их URL'ов.
+ # Для простоты, пока пустой список ��ото.
+ photos_list = []
+
+ new_product = {
+ 'name': name, 'price': price, 'description': description,
+ 'category': category if category in display_categories else 'Без категории',
+ 'photos': photos_list, 'colors': colors,
+ 'in_stock': in_stock, 'is_top': is_top
+ }
+ display_products.append(new_product)
+ current_data['products'] = display_products
+ save_data(current_data)
+ flash(f"Товар '{name}' добавлен.", 'success')
+
+ elif action == 'edit_product':
+ # Логика редактирования товара, аналогично добавлению
+ index_str = request.form.get('index')
+ if index_str is None:
+ flash("Ошибка редактирования: индекс товара не передан.", 'error')
+ return redirect(url_for('admin'))
+
+ try:
+ index = int(index_str)
+ if not (0 <= index < len(display_products)): raise IndexError("Product index out of range")
+ product_to_edit = display_products[index]
+
+ product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
+ price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.')
+ product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
+ category = request.form.get('category')
+ product_to_edit['category'] = category if category in display_categories else 'Без категории'
+ product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
+ product_to_edit['in_stock'] = 'in_stock' in request.form
+ product_to_edit['is_top'] = 'is_top' in request.form
+
+ try:
+ price = round(float(price_str), 2)
+ if price < 0: price = 0
+ product_to_edit['price'] = price
+ except ValueError:
+ flash(f"Неверный формат цены для товара '{product_to_edit['name']}'. Цена не изменена.", 'warning')
+
+ # Логика обновления фото требует загрузки на HF и удаления старых
+ # Пока пропускаем, как и в add_product
+
+ display_products[index] = product_to_edit
+ current_data['products'] = display_products
+ save_data(current_data)
+ flash(f"Товар '{product_to_edit['name']}' обновлен.", 'success')
+ except (ValueError, IndexError):
+ flash(f"Ошибка редактирования: неверный индекс товара '{index_str}'.", 'error')
+ return redirect(url_for('admin'))
+
+ elif action == 'delete_product':
+ index_str = request.form.get('index')
+ if index_str is None:
+ flash("Ошибка удаления: индекс товара не передан.", 'error')
+ return redirect(url_for('admin'))
+ try:
+ index = int(index_str)
+ if not (0 <= index < len(display_products)): raise IndexError("Product index out of range")
+ deleted_product = display_products.pop(index)
+ product_name = deleted_product.get('name', 'N/A')
+
+ # Логика удаления фото с HF, если они были загружены
+ # Пока пропускаем
+
+ current_data['products'] = display_products
+ save_data(current_data)
+ flash(f"Товар '{product_name}' удален.", 'success')
+ except (ValueError, IndexError):
+ flash(f"Ошибка удаления: неверный индекс товара '{index_str}'.", 'error')
+ return redirect(url_for('admin'))
+
+ elif action == 'delete_category':
+ # Эта логика уже покрыта выше, но на всякий случай
+ category_to_delete = request.form.get('category_name')
+ if category_to_delete and category_to_delete in display_categories:
+ display_categories.remove(category_to_delete)
+ updated_count = 0
+ for product in display_products:
+ if product.get('category') == category_to_delete:
+ product['category'] = 'Без категории'
+ updated_count += 1
+ current_data['categories'] = display_categories
+ current_data['products'] = display_products
+ save_data(current_data)
+ flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
+ else:
+ flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
+
+ else:
+ flash(f"Неизвестное действие: {action}", 'warning')
-if __name__ == '__main__':
- # Initial sync when the app starts
- initial_sync_from_hf()
+ return redirect(url_for('admin'))
+
+ except Exception as e:
+ logging.error(f"Произошла внутренняя ошибка при выполнении действия '{action}': {e}", exc_info=True)
+ flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
+ return redirect(url_for('admin'))
+
+ # Для отображения в админке
+ return render_template_string(
+ ADMIN_TEMPLATE, # Шаблон админки из кода 2
+ products=display_products,
+ categories=display_categories,
+ repo_id=GENERATED_SITES_REPO_ID, # Передаем ID репозитория для ссылок на статику
+ currency_code='KGS' # Пример валюты
+ )
+
+# Эндпоинты для принудительной синхронизации данных
+@app.route('/force_upload', methods=['POST'])
+def force_upload():
+ logging.info("Forcing upload of local data to Hugging Face...")
+ try:
+ # Нужно загрузить файл data.json
+ if not os.path.exists(DATA_FILE):
+ logging.warning(f"Local file {DATA_FILE} not found. Cannot perform forced upload.")
+ flash(f"Локальный файл {DATA_FILE} не найден. Загрузка невозможна.", 'warning')
+ return redirect(url_for('admin'))
+
+ if upload_file_to_hf(DATA_FILE, GENERATED_SITES_REPO_ID, DATA_FILE, os.getenv("HF_TOKEN_WRITE"), repo_type="dataset", commit_message=f"Force sync of {DATA_FILE}"):
+ flash("Локальные данные успешно загружены на Hugging Face.", 'success')
+ else:
+ flash("Не удалось загрузить локальные данные на Hugging Face. Проверьте логи и token.", 'error')
+ except Exception as e:
+ logging.error(f"Error during forced upload: {e}", exc_info=True)
+ flash(f"Ошибка при принудительной загрузке: {e}", 'error')
+ return redirect(url_for('admin'))
+
+@app.route('/force_download', methods=['POST'])
+def force_download():
+ logging.info("Forcing download of data from Hugging Face...")
+ try:
+ if download_db_from_hf(specific_file=DATA_FILE): # Загружаем только data.json
+ flash("Локальные данные успешно скачаны с Hugging Face. Файл data.json обновлен.", 'success')
+ load_data() # Перезагружаем данные в память после скачивания
+ else:
+ flash("Не удалось скачать данные с Hugging Face после нескольких попыток. Проверьте логи.", 'error')
+ except Exception as e:
+ logging.error(f"Error during forced download: {e}", exc_info=True)
+ flash(f"Ошибка при принудительном скачивании: {e}", 'error')
+ return redirect(url_for('admin'))
+
+# --- Вспомогательные функции для управления данными ---
+
+def load_data():
+ """Загружает данные из локального файла или скачивает с HF."""
+ default_data = {'products': [], 'categories': [], 'orders': {}} # Структура для data.json
- # Start periodic backup thread if HF_TOKEN_WRITE is available
- if HF_TOKEN_WRITE:
- backup_thread = threading.Thread(target=lambda: {
- logging.info("Periodic backup thread started."),
- initial_sync_from_hf(), # Ensure we have the latest before starting periodic saves
- periodic_backup_job() # First backup immediate
- }, daemon=True)
-
- def run_periodic_backup_loop():
- while True:
- time.sleep(BACKUP_INTERVAL_SECONDS)
- periodic_backup_job()
+ # Сначала пытаемся загрузить локально
+ try:
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
+ data = json.load(file)
+ # Проверка структуры данных
+ if not isinstance(data, dict): raise json.JSONDecodeError("Data is not a dictionary", file, 0)
+ if 'products' not in data: data['products'] = []
+ if 'categories' not in data: data['categories'] = []
+ if 'orders' not in data: data['orders'] = {}
+ logging.info(f"Local data loaded successfully from {DATA_FILE}")
+ return data
+ except FileNotFoundError:
+ logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
+ except json.JSONDecodeError as e:
+ logging.error(f"Error decoding JSON in local {DATA_FILE}: {e}. Attempting download.")
+ except Exception as e:
+ logging.error(f"Unknown error loading local {DATA_FILE}: {e}", exc_info=True)
+ logging.warning("Attempting download from HF.")
+
+ # Если локально не удалось, пробуем скачать с HF
+ if download_db_from_hf(specific_file=DATA_FILE):
+ try:
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
+ data = json.load(file)
+ if not isinstance(data, dict): raise json.JSONDecodeError("Data is not a dictionary", file, 0)
+ if 'products' not in data: data['products'] = []
+ if 'categories' not in data: data['categories'] = []
+ if 'orders' not in data: data['orders'] = {}
+ logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
+ return data
+ except FileNotFoundError:
+ logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.")
+ except json.JSONDecodeError as e:
+ logging.error(f"Error decoding JSON in downloaded {DATA_FILE}: {e}. Using default.")
+ except Exception as e:
+ logging.error(f"Unknown error loading downloaded {DATA_FILE}: {e}", exc_info=True)
+ return default_data
+ else:
+ logging.error(f"Failed to download {DATA_FILE} from HF after retries. Using empty default data structure.")
+ # Создаем пустой файл, если он не был создан при ошибке 404 в download_db_from_hf
+ if not os.path.exists(DATA_FILE):
+ try:
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
+ json.dump(default_data, f, indent=4)
+ logging.info(f"Created empty local file {DATA_FILE} after failed download.")
+ except Exception as create_e:
+ logging.error(f"Failed to create empty local file {DATA_FILE}: {create_e}")
+ return default_data
+ return default_data
+
+def save_data(data):
+ """Сохраняет данные локально и затем загружает на Hugging Face."""
+ try:
+ if not isinstance(data, dict):
+ logging.error("Attempted to save invalid data structure (not a dict). Aborting save.")
+ return
- loop_thread = threading.Thread(target=run_periodic_backup_loop, daemon=True)
+ # Убедимся, что все нужные ключи присутствуют
+ if 'products' not in data: data['products'] = []
+ if 'categories' not in data: data['categories'] = []
+ if 'orders' not in data: data['orders'] = {}
+
+ with open(DATA_FILE, 'w', encoding='utf-8') as file:
+ json.dump(data, file, ensure_ascii=False, indent=4)
+ logging.info(f"Data successfully saved to {DATA_FILE}")
- backup_thread.start() # initial sync and first backup
- loop_thread.start() # starts the loop
+ # Автоматически загружаем обновленный файл на HF
+ upload_file_to_hf(DATA_FILE, GENERATED_SITES_REPO_ID, DATA_FILE, os.getenv("HF_TOKEN_WRITE"), repo_type="dataset", commit_message=f"Sync {DATA_FILE}")
+
+ except Exception as e:
+ logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
+
+
+# --- Периодическое резервное копирование (из кода 2) ---
+def periodic_backup():
+ """Выполняет периодическое резервное копирование data.json на Hugging Face."""
+ backup_interval = 1800 # 30 минут
+ logging.info(f"Setting up periodic backup of {DATA_FILE} every {backup_interval} seconds.")
+ while True:
+ time.sleep(backup_interval)
+ logging.info("Starting periodic backup...")
+ if os.path.exists(DATA_FILE):
+ upload_file_to_hf(DATA_FILE, GENERATED_SITES_REPO_ID, DATA_FILE, os.getenv("HF_TOKEN_WRITE"), repo_type="dataset", commit_message=f"Periodic sync of {DATA_FILE}")
+ else:
+ logging.warning(f"Periodic backup skipped: {DATA_FILE} not found locally.")
+ logging.info("Periodic backup finished.")
+# --- Главный блок запуска ---
+if __name__ == '__main__':
+ # Инициализация базы данных для самого приложения EVA, если используется data.json
+ # load_data() # Загружаем данные при старте
+
+ # Запускаем поток для периодического бэкапа, если токен записи установлен
+ if os.getenv("HF_TOKEN_WRITE"):
+ backup_thread = threading.Thread(target=periodic_backup, daemon=True)
+ backup_thread.start()
+ logging.info("Periodic backup thread started.")
else:
- logging.warning("HF_TOKEN_WRITE not set. Periodic backup to Hugging Face will NOT run.")
+ logging.warning("Periodic backup will NOT run (HF_TOKEN_WRITE not set).")
+
+ logging.info("Starting Flask application EVA for site generation.")
+ # Запускаем на порту 7860, как указано в требованиях
+ app.run(debug=False, host='0.0.0.0', port=7860)
- main_app_port = int(os.getenv("MAIN_APP_PORT", 7860))
- logging.info(f"EVA - Python App Generator starting on http://localhost:{main_app_port}")
- app.run(host='0.0.0.0', port=main_app_port, debug=False) # Debug False for production/multiple processes
\ No newline at end of file
+# --- END OF FILE app (1) (9).py ---
\ No newline at end of file