Compare commits
4 Commits
ad24cb6fca
...
v1.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
| b7fad78a71 | |||
| e590a6cde0 | |||
| aad6e8c5af | |||
| 4629037890 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -3,4 +3,10 @@
|
||||
/build_cx/
|
||||
/build_linux/
|
||||
/build_win32/
|
||||
/build_darwin/
|
||||
/build_darwin/
|
||||
.idea/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
tests/__pycache__/
|
||||
build/
|
||||
dist/
|
||||
|
||||
1
app_version.py
Normal file
1
app_version.py
Normal file
@@ -0,0 +1 @@
|
||||
APP_VERSION = "1.6.0"
|
||||
78
auth_webview.py
Normal file
78
auth_webview.py
Normal file
@@ -0,0 +1,78 @@
|
||||
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()
|
||||
44
build.py
44
build.py
@@ -2,14 +2,32 @@ import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from app_version import APP_VERSION
|
||||
|
||||
# --- Конфигурация ---
|
||||
APP_NAME = "AnabasisManager"
|
||||
VERSION = "1.3" # Ваша версия
|
||||
VERSION = APP_VERSION # Единая версия приложения
|
||||
MAIN_SCRIPT = "main.py"
|
||||
ICON_PATH = "icon.ico"
|
||||
DIST_DIR = os.path.join("dist", APP_NAME)
|
||||
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():
|
||||
@@ -20,12 +38,14 @@ def run_build():
|
||||
"--noconfirm",
|
||||
"--onedir",
|
||||
"--windowed",
|
||||
"--exclude-module", "PySide6.QtWebEngineCore",
|
||||
"--exclude-module", "PySide6.QtWebEngineWidgets",
|
||||
"--exclude-module", "PySide6.QtWebEngineQuick",
|
||||
f"--name={APP_NAME}",
|
||||
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 "",
|
||||
"--collect-all", "PySide6.QtWebEngineCore",
|
||||
"--collect-all", "PySide6.QtWebEngineWidgets",
|
||||
MAIN_SCRIPT
|
||||
f"--add-data=auth_webview.py{os.pathsep}.",
|
||||
MAIN_SCRIPT
|
||||
]
|
||||
|
||||
command = [arg for arg in command if arg]
|
||||
@@ -46,16 +66,7 @@ def run_cleanup():
|
||||
if not os.path.exists(pyside_path):
|
||||
pyside_path = DIST_DIR
|
||||
|
||||
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:
|
||||
for item in REMOVE_LIST:
|
||||
path = os.path.join(pyside_path, item)
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
@@ -81,6 +92,7 @@ def create_archive():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ensure_project_root()
|
||||
# Предварительная очистка
|
||||
for folder in ["build", "dist"]:
|
||||
if os.path.exists(folder):
|
||||
@@ -91,6 +103,6 @@ if __name__ == "__main__":
|
||||
create_archive()
|
||||
|
||||
print("\n" + "=" * 30)
|
||||
print(f"ПРОЦЕСС ЗАВЕРШЕН")
|
||||
print("ПРОЦЕСС ЗАВЕРШЕН")
|
||||
print(f"Файл для отправки: dist/{ARCHIVE_NAME}.zip")
|
||||
print("=" * 30)
|
||||
print("=" * 30)
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
PySide6~=6.9.1
|
||||
PySide6~=6.10.2
|
||||
vk-api~=11.9.9
|
||||
pywebview
|
||||
|
||||
49
tests/test_auth_relogin_smoke.py
Normal file
49
tests/test_auth_relogin_smoke.py
Normal file
@@ -0,0 +1,49 @@
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user