'''Собирает MQL5.zip для четвёртой части из тега p4. Исходник сервиса берётся из рабочего дерева только после проверки, что от p4 отличаются лишь комментарии, пробелы и версия. Личные файлы не читаются. Запуск: python docs/articles/part-04/publish/build_archive.py ''' import json import re import subprocess from pathlib import Path from zipfile import ZIP_DEFLATED, ZipFile ROOT = Path(__file__).resolve().parents[4] OUTPUT = Path(__file__).resolve().parent / 'MQL5.zip' PREFIX = 'MQL5/Shared Projects/mt5-manager/' BASE = 'p4' def git(*args): return subprocess.run(['git', *args], cwd=ROOT, capture_output=True, check=True).stdout def executable(text): text = re.sub(r'^#property version[^\n]*', '', text, flags=re.M) parts = re.compile(r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'|//[^\n]*|/\*[\s\S]*?\*/') text = parts.sub(lambda m: '' if m.group().startswith(('//', '/*')) else m.group(), text) return re.findall(r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'|\w+|[^\s]', text) def build(): baseline = git('show', f'{BASE}:ServiceStateWriter.mq5').decode('utf-8-sig') service = (ROOT / 'ServiceStateWriter.mq5').read_text(encoding='utf-8-sig') if executable(baseline) != executable(service): raise RuntimeError('Сервис отличается от p4 по логике. Для архива четвёртой части нужен её исходник.') paths = git('ls-tree', '-r', '--name-only', BASE).decode().splitlines() paths = [name for name in paths if name in { 'LICENSE', 'README.md', 'ServiceStateWriter.mq5', 'config.json', 'config.py', 'deploy_service.py', 'extracticon.py', 'main.py', 'mt5_control.py', 'run.py'} or name.startswith(('static/', 'templates/'))] files = {name: git('show', f'{BASE}:{name}') for name in paths} files['ServiceStateWriter.mq5'] = service.encode('utf-8') config = json.loads(files['config.json'].decode('utf-8-sig')) config['mt5_folder'] = 'C:/MT5' config['database'] = 'C:/Users/USERNAME/AppData/Roaming/MetaQuotes/Terminal/Common/Files/mt5-manager.sqlite' for name, record in config['terminals'].items(): record['login'] = '' record['server'] = '' record.pop('password', None) record.pop('path', None) files['config.json'] = (json.dumps(config, ensure_ascii=False, indent=2) + '\n').encode('utf-8') note = '''# Архив к четвёртой части Распакуйте MQL5.zip в папку данных терминала. Проект появится в MQL5/Shared Projects/mt5-manager. Для portable-режима это папка установки MT5. Перед запуском исправьте database в config.json: замените USERNAME на имя пользователя Windows. Проверьте mt5_folder и имена уже установленных терминалов. Логины и серверы в примере оставлены пустыми; используются счета, сохранённые MT5. Откройте ServiceStateWriter.mq5 в MetaEditor и скомпилируйте его. Полученный EX5 будет расположен в MQL5/Services/Shared Projects/mt5-manager. Скопируйте EX5 по этому же пути в остальные терминалы и static/services.ini в их папки config. Либо используйте deploy_service.py, как описано в статье. Архив содержит код версии p4 с исправленным оформлением сервиса 1.01. Возможности пятой части в него не включены. Исполняемые EX5 не распространяются. --- ''' files['README.md'] = note.encode('utf-8') + files['README.md'] with ZipFile(OUTPUT, 'w', compression=ZIP_DEFLATED) as archive: for name, data in sorted(files.items()): if name.lower().endswith('.ex5'): raise RuntimeError('EX5 запрещён в архиве публикации.') archive.writestr(PREFIX + name, data) with ZipFile(OUTPUT) as archive: assert archive.testzip() is None assert all(name.startswith('MQL5/') for name in archive.namelist()) assert not any(name.lower().endswith('.ex5') for name in archive.namelist()) print(f'MQL5.zip: {len(files)} файлов, {OUTPUT.stat().st_size} байт; база p4, без EX5 и личных реквизитов.') if __name__ == '__main__': build()