Compare commits
11 Commits
v1.6.3
...
e97d346682
| Author | SHA1 | Date | |
|---|---|---|---|
| e97d346682 | |||
| 3ed5bba9af | |||
| 1eab8651f2 | |||
|
|
ea188ffc13 | ||
|
|
6aa50b03da | ||
|
|
86aa2ddc1a | ||
|
|
32e30f5484 | ||
|
|
194d696430 | ||
|
|
0dbd71c036 | ||
|
|
134aa88f22 | ||
|
|
0c270a6cb1 |
@@ -1,35 +0,0 @@
|
|||||||
name: Desktop CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: https://git.daemonlord.ru/actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: https://git.daemonlord.ru/actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.13"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
- name: Validate syntax
|
|
||||||
run: |
|
|
||||||
python -m py_compile app_version.py main.py build.py tests/test_auth_relogin_smoke.py
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
python -m unittest tests/test_auth_relogin_smoke.py
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
name: Desktop Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: windows
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: https://git.daemonlord.ru/actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
tags: true
|
|
||||||
|
|
||||||
- name: Ensure Python 3.13
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
if (Get-Command python -ErrorAction SilentlyContinue) {
|
|
||||||
python --version
|
|
||||||
} elseif (Get-Command py -ErrorAction SilentlyContinue) {
|
|
||||||
$pyExe = py -3.13 -c "import sys; print(sys.executable)"
|
|
||||||
if (-not $pyExe) {
|
|
||||||
throw "Python 3.13 launcher is available, but interpreter was not found."
|
|
||||||
}
|
|
||||||
Split-Path $pyExe | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
|
||||||
python --version
|
|
||||||
} else {
|
|
||||||
throw "Python is not installed on runner. Install Python 3.13 and restart runner service."
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt pyinstaller
|
|
||||||
|
|
||||||
- name: Extract app version
|
|
||||||
id: extract_version
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$version = (python -c "from app_version import APP_VERSION; print(APP_VERSION)").Trim()
|
|
||||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
|
||||||
[System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "version=$version`n", $utf8NoBom)
|
|
||||||
Write-Host "Detected version: $version"
|
|
||||||
|
|
||||||
- name: Stop if version already released
|
|
||||||
id: stop
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$version = "${{ steps.extract_version.outputs.version }}"
|
|
||||||
$tag = "v$version"
|
|
||||||
git show-ref --tags --quiet --verify "refs/tags/$tag"
|
|
||||||
$tagExists = ($LASTEXITCODE -eq 0)
|
|
||||||
$global:LASTEXITCODE = 0
|
|
||||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
|
||||||
if ($tagExists) {
|
|
||||||
Write-Host "Version $tag already released, stopping job."
|
|
||||||
[System.IO.File]::AppendAllText($env:GITHUB_ENV, "CONTINUE=false`n", $utf8NoBom)
|
|
||||||
} else {
|
|
||||||
Write-Host "Version $tag not released yet, continuing workflow..."
|
|
||||||
[System.IO.File]::AppendAllText($env:GITHUB_ENV, "CONTINUE=true`n", $utf8NoBom)
|
|
||||||
}
|
|
||||||
exit 0
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
if: env.CONTINUE == 'true'
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
python -m py_compile app_version.py main.py build.py tests/test_auth_relogin_smoke.py
|
|
||||||
python -m unittest tests/test_auth_relogin_smoke.py
|
|
||||||
|
|
||||||
- name: Build release zip
|
|
||||||
if: env.CONTINUE == 'true'
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
python build.py
|
|
||||||
|
|
||||||
- name: Ensure archive exists
|
|
||||||
if: env.CONTINUE == 'true'
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$version = "${{ steps.extract_version.outputs.version }}"
|
|
||||||
$archivePath = "dist/AnabasisManager-$version.zip"
|
|
||||||
if (-not (Test-Path $archivePath)) {
|
|
||||||
throw "Archive not found: $archivePath"
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Generate SHA256 checksum
|
|
||||||
if: env.CONTINUE == 'true'
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$version = "${{ steps.extract_version.outputs.version }}"
|
|
||||||
$archiveName = "AnabasisManager-$version.zip"
|
|
||||||
$archivePath = "dist/$archiveName"
|
|
||||||
$checksumPath = "dist/$archiveName.sha256"
|
|
||||||
$hash = (Get-FileHash -Path $archivePath -Algorithm SHA256).Hash.ToLower()
|
|
||||||
"$hash $archiveName" | Set-Content -Path $checksumPath -Encoding UTF8
|
|
||||||
Write-Host "Checksum created: $checksumPath"
|
|
||||||
|
|
||||||
- name: Configure git identity
|
|
||||||
if: env.CONTINUE == 'true'
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
git config user.name "gitea-actions"
|
|
||||||
git config user.email "gitea-actions@daemonlord.ru"
|
|
||||||
|
|
||||||
- name: Create git tag
|
|
||||||
if: env.CONTINUE == 'true'
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$version = "${{ steps.extract_version.outputs.version }}"
|
|
||||||
$tag = "v$version"
|
|
||||||
git tag "$tag"
|
|
||||||
git push origin "$tag"
|
|
||||||
|
|
||||||
- name: Create Gitea Release
|
|
||||||
if: env.CONTINUE == 'true'
|
|
||||||
uses: https://git.daemonlord.ru/actions/gitea-release-action@v1
|
|
||||||
with:
|
|
||||||
server_url: https://git.daemonlord.ru
|
|
||||||
repository: ${{ gitea.repository }}
|
|
||||||
token: ${{ secrets.API_TOKEN }}
|
|
||||||
tag_name: v${{ steps.extract_version.outputs.version }}
|
|
||||||
name: Anabasis Manager ${{ steps.extract_version.outputs.version }}
|
|
||||||
body: |
|
|
||||||
Desktop release v${{ steps.extract_version.outputs.version }}
|
|
||||||
files: |
|
|
||||||
dist/AnabasisManager-${{ steps.extract_version.outputs.version }}.zip
|
|
||||||
dist/AnabasisManager-${{ steps.extract_version.outputs.version }}.zip.sha256
|
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -4,10 +4,3 @@
|
|||||||
/build_linux/
|
/build_linux/
|
||||||
/build_win32/
|
/build_win32/
|
||||||
/build_darwin/
|
/build_darwin/
|
||||||
.idea/
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
tests/__pycache__/
|
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
AnabasisManager.spec
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
APP_VERSION = "1.6.3"
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import json
|
|
||||||
import threading
|
|
||||||
from urllib.parse import urlparse, parse_qs, unquote
|
|
||||||
|
|
||||||
import webview
|
|
||||||
|
|
||||||
|
|
||||||
def extract_token(url_string):
|
|
||||||
token = None
|
|
||||||
expires_in = 3600
|
|
||||||
parsed = urlparse(url_string)
|
|
||||||
|
|
||||||
if parsed.fragment:
|
|
||||||
params = parse_qs(parsed.fragment)
|
|
||||||
else:
|
|
||||||
params = parse_qs(parsed.query)
|
|
||||||
|
|
||||||
if 'access_token' in params:
|
|
||||||
token = params['access_token'][0]
|
|
||||||
if 'expires_in' in params:
|
|
||||||
try:
|
|
||||||
expires_in = int(params['expires_in'][0])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not token:
|
|
||||||
start_marker = "access_token%253D"
|
|
||||||
end_marker = "%25"
|
|
||||||
|
|
||||||
start_index = url_string.find(start_marker)
|
|
||||||
if start_index != -1:
|
|
||||||
token_start_index = start_index + len(start_marker)
|
|
||||||
remaining_url = url_string[token_start_index:]
|
|
||||||
end_index = remaining_url.find(end_marker)
|
|
||||||
|
|
||||||
if end_index != -1:
|
|
||||||
raw_token = remaining_url[:end_index]
|
|
||||||
else:
|
|
||||||
amp_index = remaining_url.find('&')
|
|
||||||
if amp_index != -1:
|
|
||||||
raw_token = remaining_url[:amp_index]
|
|
||||||
else:
|
|
||||||
raw_token = remaining_url
|
|
||||||
token = unquote(raw_token)
|
|
||||||
|
|
||||||
return token, expires_in
|
|
||||||
|
|
||||||
|
|
||||||
def main_auth(auth_url, output_path):
|
|
||||||
def poll_url():
|
|
||||||
try:
|
|
||||||
url = window.get_current_url()
|
|
||||||
except Exception:
|
|
||||||
url = None
|
|
||||||
if url:
|
|
||||||
token, expires_in = extract_token(url)
|
|
||||||
if token:
|
|
||||||
data = {"token": token, "expires_in": expires_in}
|
|
||||||
with open(output_path, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(data, f)
|
|
||||||
window.destroy()
|
|
||||||
return
|
|
||||||
threading.Timer(0.5, poll_url).start()
|
|
||||||
|
|
||||||
def on_loaded():
|
|
||||||
threading.Timer(0.5, poll_url).start()
|
|
||||||
|
|
||||||
window = webview.create_window("VK Авторизация", auth_url)
|
|
||||||
window.events.loaded += on_loaded
|
|
||||||
storage_path = os.path.join(os.path.dirname(output_path), "webview_profile")
|
|
||||||
webview.start(private_mode=False, storage_path=storage_path)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
40
build.py
40
build.py
@@ -2,32 +2,14 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from app_version import APP_VERSION
|
|
||||||
|
|
||||||
# --- Конфигурация ---
|
# --- Конфигурация ---
|
||||||
APP_NAME = "AnabasisManager"
|
APP_NAME = "AnabasisManager"
|
||||||
VERSION = APP_VERSION # Единая версия приложения
|
VERSION = "1.3" # Ваша версия
|
||||||
MAIN_SCRIPT = "main.py"
|
MAIN_SCRIPT = "main.py"
|
||||||
ICON_PATH = "icon.ico"
|
ICON_PATH = "icon.ico"
|
||||||
DIST_DIR = os.path.join("dist", APP_NAME)
|
DIST_DIR = os.path.join("dist", APP_NAME)
|
||||||
ARCHIVE_NAME = f"{APP_NAME}-{VERSION}" # Формат Название-Версия
|
ARCHIVE_NAME = f"{APP_NAME}-{VERSION}" # Формат Название-Версия
|
||||||
SAFE_CLEAN_ROOT_FILES = {"main.py", "requirements.txt", "build.py"}
|
|
||||||
REMOVE_LIST = [
|
|
||||||
"Qt6Pdf.dll", "Qt6PdfQuick.dll", "Qt6PdfWidgets.dll",
|
|
||||||
"Qt6VirtualKeyboard.dll", "Qt6Positioning.dll",
|
|
||||||
"Qt6PrintSupport.dll", "Qt6Svg.dll", "Qt6Sql.dll",
|
|
||||||
"Qt6Charts.dll", "Qt6Multimedia.dll", "Qt63DCore.dll",
|
|
||||||
"translations",
|
|
||||||
"Qt6QuickTemplates2.dll"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_project_root():
|
|
||||||
missing = [name for name in SAFE_CLEAN_ROOT_FILES if not os.path.exists(name)]
|
|
||||||
if missing:
|
|
||||||
print("[ERROR] Скрипт нужно запускать из корня проекта.")
|
|
||||||
print(f"[ERROR] Не найдены: {', '.join(missing)}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def run_build():
|
def run_build():
|
||||||
@@ -38,13 +20,11 @@ def run_build():
|
|||||||
"--noconfirm",
|
"--noconfirm",
|
||||||
"--onedir",
|
"--onedir",
|
||||||
"--windowed",
|
"--windowed",
|
||||||
"--exclude-module", "PySide6.QtWebEngineCore",
|
|
||||||
"--exclude-module", "PySide6.QtWebEngineWidgets",
|
|
||||||
"--exclude-module", "PySide6.QtWebEngineQuick",
|
|
||||||
f"--name={APP_NAME}",
|
f"--name={APP_NAME}",
|
||||||
f"--icon={ICON_PATH}" if os.path.exists(ICON_PATH) else "",
|
f"--icon={ICON_PATH}" if os.path.exists(ICON_PATH) else "",
|
||||||
f"--add-data={ICON_PATH}{os.pathsep}." if os.path.exists(ICON_PATH) else "",
|
f"--add-data={ICON_PATH}{os.pathsep}." if os.path.exists(ICON_PATH) else "",
|
||||||
f"--add-data=auth_webview.py{os.pathsep}.",
|
"--collect-all", "PySide6.QtWebEngineCore",
|
||||||
|
"--collect-all", "PySide6.QtWebEngineWidgets",
|
||||||
MAIN_SCRIPT
|
MAIN_SCRIPT
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -66,7 +46,16 @@ def run_cleanup():
|
|||||||
if not os.path.exists(pyside_path):
|
if not os.path.exists(pyside_path):
|
||||||
pyside_path = DIST_DIR
|
pyside_path = DIST_DIR
|
||||||
|
|
||||||
for item in REMOVE_LIST:
|
to_remove = [
|
||||||
|
"Qt6Pdf.dll", "Qt6PdfQuick.dll", "Qt6PdfWidgets.dll",
|
||||||
|
"Qt6VirtualKeyboard.dll", "Qt6Positioning.dll",
|
||||||
|
"Qt6PrintSupport.dll", "Qt6Svg.dll", "Qt6Sql.dll",
|
||||||
|
"Qt6Charts.dll", "Qt6Multimedia.dll", "Qt63DCore.dll",
|
||||||
|
"translations",
|
||||||
|
"Qt6QuickTemplates2.dll"
|
||||||
|
]
|
||||||
|
|
||||||
|
for item in to_remove:
|
||||||
path = os.path.join(pyside_path, item)
|
path = os.path.join(pyside_path, item)
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
try:
|
try:
|
||||||
@@ -92,7 +81,6 @@ def create_archive():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
ensure_project_root()
|
|
||||||
# Предварительная очистка
|
# Предварительная очистка
|
||||||
for folder in ["build", "dist"]:
|
for folder in ["build", "dist"]:
|
||||||
if os.path.exists(folder):
|
if os.path.exists(folder):
|
||||||
@@ -103,6 +91,6 @@ if __name__ == "__main__":
|
|||||||
create_archive()
|
create_archive()
|
||||||
|
|
||||||
print("\n" + "=" * 30)
|
print("\n" + "=" * 30)
|
||||||
print("ПРОЦЕСС ЗАВЕРШЕН")
|
print(f"ПРОЦЕСС ЗАВЕРШЕН")
|
||||||
print(f"Файл для отправки: dist/{ARCHIVE_NAME}.zip")
|
print(f"Файл для отправки: dist/{ARCHIVE_NAME}.zip")
|
||||||
print("=" * 30)
|
print("=" * 30)
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
PySide6~=6.10.2
|
PySide6~=6.9.1
|
||||||
vk-api~=11.9.9
|
vk-api~=11.9.9
|
||||||
pywebview
|
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
class AuthReloginSmokeTests(unittest.TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
cls.source = Path("main.py").read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
def test_auth_command_builder_handles_frozen_and_source(self):
|
|
||||||
self.assertIn("def _build_auth_command(self, auth_url, output_path):", self.source)
|
|
||||||
self.assertIn('return sys.executable, ["--auth", auth_url, output_path]', self.source)
|
|
||||||
self.assertIn('return sys.executable, [os.path.abspath(__file__), "--auth", auth_url, output_path]', self.source)
|
|
||||||
|
|
||||||
def test_auth_runs_via_qprocess(self):
|
|
||||||
self.assertIn("process = QProcess(self)", self.source)
|
|
||||||
self.assertIn("process.start(program, args)", self.source)
|
|
||||||
self.assertIn("def _on_auth_process_finished(self, exit_code, _exit_status):", self.source)
|
|
||||||
self.assertIn("if self.auth_process and self.auth_process.state() == QProcess.NotRunning:", self.source)
|
|
||||||
|
|
||||||
def test_force_relogin_has_backoff_and_event_log(self):
|
|
||||||
self.assertIn("AUTH_RELOGIN_BACKOFF_SECONDS = 5.0", self.source)
|
|
||||||
self.assertIn("if self._auth_relogin_in_progress:", self.source)
|
|
||||||
self.assertIn("force_relogin_backoff", self.source)
|
|
||||||
self.assertIn("force_relogin", self.source)
|
|
||||||
|
|
||||||
def test_auth_error_paths_trigger_force_relogin(self):
|
|
||||||
self.assertIn("def _handle_vk_api_error(self, context, exc, action_name=None, ui_message_prefix=None, disable_ui=False):", self.source)
|
|
||||||
self.assertIn("self._force_relogin(exc, action_name or context)", self.source)
|
|
||||||
self.assertIn('"load_chats",', self.source)
|
|
||||||
self.assertIn('"execute_user_action",', self.source)
|
|
||||||
self.assertIn('"set_user_admin",', self.source)
|
|
||||||
|
|
||||||
def test_tab_checkbox_lists_use_existing_attributes(self):
|
|
||||||
self.assertIn("self.warehouse_chat_checkboxes", self.source)
|
|
||||||
self.assertIn("self.coffee_chat_checkboxes", self.source)
|
|
||||||
self.assertNotIn("self.retail_warehouse_checkboxes", self.source)
|
|
||||||
self.assertNotIn("self.retail_coffee_checkboxes", self.source)
|
|
||||||
|
|
||||||
def test_update_check_actions_exist(self):
|
|
||||||
self.assertIn("from app_version import APP_VERSION", self.source)
|
|
||||||
self.assertIn("UPDATE_REPOSITORY = ", self.source)
|
|
||||||
self.assertIn('QAction("Проверить обновления", self)', self.source)
|
|
||||||
self.assertIn("def check_for_updates(self, silent_no_updates=False):", self.source)
|
|
||||||
self.assertIn("class UpdateChecker(QObject):", self.source)
|
|
||||||
self.assertIn('message_box.addButton("Обновить сейчас", QMessageBox.AcceptRole)', self.source)
|
|
||||||
self.assertIn("def _start_auto_update(self, download_url, latest_version, checksum_url=\"\", download_name=\"\"):", self.source)
|
|
||||||
self.assertIn("def _verify_update_checksum(self, zip_path, checksum_url, download_name):", self.source)
|
|
||||||
self.assertIn("def _build_update_script(self, app_dir, source_dir, exe_name, target_pid):", self.source)
|
|
||||||
self.assertIn("set TARGET_PID=", self.source)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
Reference in New Issue
Block a user