geforkt von antekov/mt5-manager
Describe simplified setup, optional server catalogs, independent connection tests, safe instance deletion, and per-terminal operation notifications. Add reproducible publication builders, refresh the source archive and listings, and record the remaining website and screenshot update steps.
'''Сборка архива исходников пятой части и согласованного дерева в статье.
| |
| |
Запуск: python docs/articles/part-05/publish/build_archive.py
| |
Список файлов явный: рабочая конфигурация, EX5, EXE и каталоги пользователя
| |
никогда не добавляются. config.json создаётся из config.example.json.
| |
'''
| |
| |
import hashlib
| |
import json
| |
import re
| |
import subprocess
| |
from pathlib import Path, PurePosixPath
| |
from zipfile import ZIP_DEFLATED, ZipFile
| |
| |
ROOT = Path(__file__).resolve().parents[4]
| |
OUT = Path(__file__).resolve().parent
| |
ARTICLE = OUT.parent / 'draft.md'
| |
PREFIX = 'MQL5/Shared Projects/mt5-manager/'
| |
SERIES = 'https://www.mql5.com/ru/blogs/post/765539'
| |
| |
# Таблица строится из этого списка, но показывает только основные рабочие файлы.
| |
FILES = [
| |
('README.md', 'Быстрый старт и ссылки на документацию', 5),
| |
('LICENSE', 'Лицензия проекта', 1),
| |
('config.example.json', 'Пример конфигурации без счетов', 5),
| |
('config.json', 'Начальная конфигурация, созданная из примера', 5),
| |
('config.py', 'Чтение, проверка и сохранение конфигурации', 5),
| |
('terminal_manager.py', 'Создание экземпляров, фоновые задачи и настройка счёта', 5),
| |
('project_updates.py', 'Обновление Git, сборка и распространение сервиса', 5),
| |
('main.py', 'Маршруты настроек, файлов и операций с терминалами', 5),
| |
('mt5_control.py', 'Процессы и чтение состояния без Python API MetaTrader5', 5),
| |
('requirements.txt', 'Зависимости приложения', 5),
| |
('deploy_service.py', 'Развёртывание и компиляция сервиса', 5),
| |
('run.py', 'Запуск веб-приложения', 5),
| |
('ServiceStateWriter.mq5', 'Сервис записи состояния в SQLite', 5),
| |
('templates/base.html', 'Общая навигация и оформление страниц', 5),
| |
('templates/index.html', 'Главная страница и создание экземпляра', 5),
| |
('templates/settings.html', 'Страница настроек и загрузки файлов', 5),
| |
('static/script.js', 'Карточки, поиск, фильтры и управление операциями', 5),
| |
('static/manager.js', 'Запросы API, сообщения и диалоговые окна', 5),
| |
('static/settings.js', 'Формы настроек, файлов и счетов', 5),
| |
('static/styles.css', 'Общие стили и адаптивная компоновка', 5),
| |
('static/services.ini', 'Образец автозапуска сервиса', 5),
| |
('static/img/MetaQuotes-Demo.ico', 'Значок торгового сервера', 3),
| |
('docs/README.md', 'Оглавление документации и текущее состояние', 5),
| |
('docs/installation.md', 'Установка и первый запуск', 5),
| |
('docs/configuration.md', 'Настройки и формат конфигурации', 5),
| |
('docs/user-guide.md', 'Работа через веб-интерфейс', 5),
| |
('docs/maintenance.md', 'Обновление и устранение неполадок', 5),
| |
('docs/architecture.md', 'Архитектура, API и проверки', 5),
| |
]
| |
| |
ARCHIVE_NOTE = '''# Установка из архива пятой части
| |
| |
Распакуйте MQL5.zip в папку данных установленного терминала. Проект окажется
| |
в MQL5/Shared Projects/mt5-manager. В portable-режиме папка данных совпадает
| |
с папкой установки. Откройте PowerShell в папке проекта и следуйте быстрому
| |
старту ниже, начиная с создания Python-окружения: клонировать ещё одну копию
| |
репозитория поверх распакованного проекта не нужно.
| |
| |
config.json и config.example.json содержат одинаковый пример без счетов.
| |
Не используйте буквальный USERNAME в пути базы: команды быстрого старта
| |
подставляют путь текущего пользователя и создают личный config.local.json.
| |
| |
EXE, EX5 и servers.dat не распространяются в архиве. Укажите исходную установку
| |
MT5 в настройках: из неё менеджер возьмёт оба EXE, а сервис соберёт из MQ5 проекта.
| |
Если EXE отсутствуют, загрузите их в выбранную папку через веб-интерфейс.
| |
servers.dat можно передать отдельно при создании экземпляра, если он необходим.
| |
| |
В архиве нет каталога .git. Проверка и загрузка обновлений Git требуют клонирования
| |
репозитория с настроенным upstream; сборка и копирование сервиса доступны и без Git.
| |
Материалы статей остаются в репозитории и на MQL5, в архив включены руководства
| |
по текущему приложению.
| |
Каталоги tests и tools в архив не включены; проверки и вспомогательные инструменты
| |
доступны в репозитории проекта.
| |
| |
---
| |
| |
'''
| |
| |
| |
def file_version(path, data):
| |
if Path(path).suffix not in ('.py', '.mq5', '.js', '.css', '.html'):
| |
return '—'
| |
text = data.decode('utf-8-sig')
| |
for pattern in (r'__version__\s*=\s*[\'\"]([^\'\"]+)', r'#property\s+version\s+"([^"]+)"',
| |
r'@version\s+(\d[\d.]*)', r'version\s+(\d[\d.]*)'):
| |
match = re.search(pattern, text)
| |
if match:
| |
return match.group(1)
| |
return '—'
| |
| |
| |
def archive_styles(data):
| |
# Убираем из публикуемой копии правила для иконок, не входящих в архив.
| |
included = {name for name, _, _ in FILES}
| |
text = data.decode('utf-8-sig')
| |
| |
def keep_rule(match):
| |
icons = re.findall(r'''url\(['"]?/static/img/([^)'"\s]+)['"]?\)''', match.group())
| |
return match.group() if all('static/img/' + icon in included for icon in icons) else ''
| |
| |
return re.sub(r'(?m)^\.server-[^\n{}]*\{[^\n{}]*\}\r?\n?', keep_rule, text).encode('utf-8')
| |
| |
| |
def build():
| |
# Принципиально не читаем личный config.json из рабочего дерева.
| |
example = (ROOT / 'config.example.json').read_bytes()
| |
config = json.loads(example.decode('utf-8-sig'))
| |
if config.get('terminals') != {}:
| |
raise RuntimeError('Пример для публикации должен содержать пустой список счетов.')
| |
files, manifest = {}, []
| |
for name, description, part in FILES:
| |
if Path(name).suffix.lower() in ('.ex5', '.exe', '.dll', '.dat'):
| |
raise RuntimeError('В манифесте обнаружен непубликуемый файл.')
| |
data = example if name == 'config.json' else (ROOT / name).read_bytes()
| |
version = file_version(name, data)
| |
if name == 'static/styles.css':
| |
data = archive_styles(data)
| |
if name == 'docs/architecture.md':
| |
text = data.decode('utf-8-sig').replace(
| |
'## Проверки разработки\n',
| |
'## Проверки разработки\n\nКаталоги `tests` и `tools` доступны '
| |
'в [репозитории проекта](https://forge.mql5.io/antekov/mt5-manager), '
| |
'но не входят в архив к статье. Следующие команды предназначены '
| |
'для рабочей копии репозитория.\n')
| |
data = text.encode('utf-8')
| |
if name in ('README.md', 'docs/README.md'):
| |
text = data.decode('utf-8-sig')
| |
# В ZIP нет каталога рукописей; ссылки ведут к циклу на сайте.
| |
text = text.replace('docs/articles/README.md', SERIES).replace('articles/README.md', SERIES)
| |
if name == 'README.md':
| |
text = ARCHIVE_NOTE + text
| |
data = text.encode('utf-8')
| |
files[name] = data
| |
manifest.append({'path': name, 'archive_path': PREFIX + name,
| |
'version': version, 'description': description, 'part': part,
| |
'sha256': hashlib.sha256(data).hexdigest()})
| |
| |
OUT.mkdir(parents=True, exist_ok=True)
| |
archive_path = OUT / 'MQL5.zip'
| |
with ZipFile(archive_path, 'w', compression=ZIP_DEFLATED) as archive:
| |
for name, data in files.items():
| |
archive.writestr(PREFIX + name, data)
| |
with ZipFile(archive_path) as archive:
| |
if archive.testzip() is not None:
| |
raise RuntimeError('Архив не прошёл проверку целостности.')
| |
if set(archive.namelist()) != {PREFIX + name for name in files}:
| |
raise RuntimeError('Содержимое архива отличается от манифеста.')
| |
revision = subprocess.run(['git', 'rev-parse', 'HEAD'], cwd=ROOT, capture_output=True,
| |
text=True, check=True).stdout.strip()
| |
(OUT / 'archive-manifest.json').write_text(json.dumps(
| |
{'base_revision': revision, 'files': manifest}, ensure_ascii=False, indent=2) + '\n',
| |
encoding='utf-8', newline='\n')
| |
| |
# Основные файлы в таблице получают версии из манифеста архива.
| |
tree = {}
| |
entries = {item['path']: item for item in manifest}
| |
for name in files:
| |
if name in ('README.md', 'LICENSE', 'config.example.json') or name.startswith('static/img/'):
| |
continue
| |
branch = tree
| |
parts = PurePosixPath(name).parts
| |
for component in parts[:-1]:
| |
branch = branch.setdefault(component, {})
| |
branch[parts[-1]] = None
| |
tree['static'] = {'…': None, **tree['static']}
| |
rows = ['| # | Имя | Версия | Описание | Последние изменения |',
| |
'|---|---|---|---|---|',
| |
'| | **MQL5/Shared Projects/mt5-manager/** | | Рабочая папка проекта | |']
| |
number = 0
| |
| |
def render(branch, parents=(), indent=''):
| |
nonlocal number
| |
for index, (name, children) in enumerate(branch.items()):
| |
last = index == len(branch) - 1
| |
label = indent + ('└─ ' if last else '├─ ') + name
| |
label = label.replace(' ', ' ')
| |
if not parents and name == 'docs':
| |
rows.append(f'| | **{label}/** | — | Документация по установке, настройке и работе приложения | Часть 5 |')
| |
elif parents == ('static',) and name == '…':
| |
rows.append(f'| | {label} | — | Дополнительные статические ресурсы | |')
| |
elif children is not None:
| |
rows.append(f'| | **{label}/** | | | |')
| |
render(children, (*parents, name), indent + (' ' if last else '│ '))
| |
else:
| |
number += 1
| |
item = entries['/'.join((*parents, name))]
| |
part = item['part']
| |
origin = 'Часть 5' if part == 5 else f'[Часть {part}](https://www.mql5.com/ru/articles/{20150 if part == 4 else 19946 if part == 3 else 19804})'
| |
rows.append(f'| {number} | {label} | {item["version"]} | {item["description"]} | {origin} |')
| |
render(tree)
| |
article = ARTICLE.read_text(encoding='utf-8')
| |
before = article.split('## Содержание архива', 1)[0]
| |
note = ('## Содержание архива\n\nАрхив `MQL5.zip` распаковывается в папку данных терминала: '
| |
'все файлы проекта находятся внутри `MQL5/Shared Projects/mt5-manager`. '
| |
'Скомпилированные EX5, исполняемые файлы терминала и личные настройки '
| |
'в него не входят. Файл `config.json` в архиве создан из безопасного примера '
| |
'`config.example.json` с пустым списком счетов.\n\n')
| |
tail = '\n\nИсходный код проекта доступен в репозитории [mt5-manager](https://forge.mql5.io/antekov/mt5-manager).\n'
| |
ARTICLE.write_text(before + note + '\n'.join(rows) + tail, encoding='utf-8', newline='\n')
| |
print(f'MQL5.zip: {len(files)} files; article table: {number} main files and documentation directory.')
| |
| |
| |
if __name__ == '__main__':
| |
build()
|