proker 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- proker-2.0.0/MANIFEST.in +7 -0
- proker-2.0.0/PKG-INFO +23 -0
- proker-2.0.0/proker.egg-info/PKG-INFO +23 -0
- proker-2.0.0/proker.egg-info/SOURCES.txt +19 -0
- proker-2.0.0/proker.egg-info/dependency_links.txt +1 -0
- proker-2.0.0/proker.egg-info/entry_points.txt +2 -0
- proker-2.0.0/proker.egg-info/requires.txt +3 -0
- proker-2.0.0/proker.egg-info/top_level.txt +1 -0
- proker-2.0.0/proteomicsviewer/__init__.py +3 -0
- proker-2.0.0/proteomicsviewer/__main__.py +7 -0
- proker-2.0.0/proteomicsviewer/cli.py +507 -0
- proker-2.0.0/proteomicsviewer/icon.py +154 -0
- proker-2.0.0/proteomicsviewer/install_shortcut.py +219 -0
- proker-2.0.0/proteomicsviewer/server/__init__.py +0 -0
- proker-2.0.0/proteomicsviewer/server/main.py +121 -0
- proker-2.0.0/proteomicsviewer/server/parser.py +186 -0
- proker-2.0.0/proteomicsviewer/server/state.py +14 -0
- proker-2.0.0/proteomicsviewer/server/templates/index.html +1760 -0
- proker-2.0.0/proteomicsviewer/server/templates/logo.svg +34 -0
- proker-2.0.0/pyproject.toml +49 -0
- proker-2.0.0/setup.cfg +4 -0
proker-2.0.0/MANIFEST.in
ADDED
proker-2.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proker
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Pro-ker Proteomics Viewer — an interactive browser-based proteomics data visualization tool
|
|
5
|
+
Author-email: Billy Ngo <billy.ngo0108@gmail.com>
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/billy-ngo/proteomics-viewer
|
|
8
|
+
Project-URL: Repository, https://github.com/billy-ngo/proteomics-viewer
|
|
9
|
+
Keywords: proteomics,mass-spectrometry,visualization,MaxQuant,bioinformatics,pro-ker
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: fastapi>=0.111.0
|
|
22
|
+
Requires-Dist: uvicorn[standard]>=0.29.0
|
|
23
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proker
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Pro-ker Proteomics Viewer — an interactive browser-based proteomics data visualization tool
|
|
5
|
+
Author-email: Billy Ngo <billy.ngo0108@gmail.com>
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/billy-ngo/proteomics-viewer
|
|
8
|
+
Project-URL: Repository, https://github.com/billy-ngo/proteomics-viewer
|
|
9
|
+
Keywords: proteomics,mass-spectrometry,visualization,MaxQuant,bioinformatics,pro-ker
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: fastapi>=0.111.0
|
|
22
|
+
Requires-Dist: uvicorn[standard]>=0.29.0
|
|
23
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
pyproject.toml
|
|
3
|
+
proker.egg-info/PKG-INFO
|
|
4
|
+
proker.egg-info/SOURCES.txt
|
|
5
|
+
proker.egg-info/dependency_links.txt
|
|
6
|
+
proker.egg-info/entry_points.txt
|
|
7
|
+
proker.egg-info/requires.txt
|
|
8
|
+
proker.egg-info/top_level.txt
|
|
9
|
+
proteomicsviewer/__init__.py
|
|
10
|
+
proteomicsviewer/__main__.py
|
|
11
|
+
proteomicsviewer/cli.py
|
|
12
|
+
proteomicsviewer/icon.py
|
|
13
|
+
proteomicsviewer/install_shortcut.py
|
|
14
|
+
proteomicsviewer/server/__init__.py
|
|
15
|
+
proteomicsviewer/server/main.py
|
|
16
|
+
proteomicsviewer/server/parser.py
|
|
17
|
+
proteomicsviewer/server/state.py
|
|
18
|
+
proteomicsviewer/server/templates/index.html
|
|
19
|
+
proteomicsviewer/server/templates/logo.svg
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
proteomicsviewer
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pro-ker Proteomics Analysis CLI — launch the viewer from the command line.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
proker # start on default port 8050
|
|
6
|
+
proker data.txt # start and auto-load a file
|
|
7
|
+
proker --port 9000 # start on a custom port
|
|
8
|
+
proker --no-browser # start without opening the browser
|
|
9
|
+
proker --install # create a desktop shortcut
|
|
10
|
+
proker --update # check for updates and install if available
|
|
11
|
+
proker --no-update # skip the automatic update check
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import atexit
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
import webbrowser
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
_CONFIG_DIR = Path.home() / ".proker"
|
|
26
|
+
_LOCK_FILE = _CONFIG_DIR / "server.lock"
|
|
27
|
+
_FIRST_RUN_MARKER = _CONFIG_DIR / ".shortcut_prompted"
|
|
28
|
+
_UPDATE_CHECK_FILE = _CONFIG_DIR / ".last_update_check"
|
|
29
|
+
_VERSION_MARKER = _CONFIG_DIR / ".installed_version"
|
|
30
|
+
|
|
31
|
+
_UPDATE_CHECK_INTERVAL = 3600 # 1 hour
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ── Welcome message ──────────────────────────────────────────────
|
|
35
|
+
def _show_welcome_if_new():
|
|
36
|
+
ver = _get_installed_version()
|
|
37
|
+
try:
|
|
38
|
+
prev = _VERSION_MARKER.read_text().strip() if _VERSION_MARKER.exists() else None
|
|
39
|
+
except Exception:
|
|
40
|
+
prev = None
|
|
41
|
+
|
|
42
|
+
if prev == ver:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
_VERSION_MARKER.write_text(ver)
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
if sys.stdout is None:
|
|
53
|
+
return
|
|
54
|
+
sys.stdout.write('')
|
|
55
|
+
except Exception:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
is_upgrade = prev is not None
|
|
59
|
+
print()
|
|
60
|
+
print(f" {'=' * 44}")
|
|
61
|
+
if is_upgrade:
|
|
62
|
+
print(f" Pro-ker Proteomics Analysis updated to v{ver}")
|
|
63
|
+
else:
|
|
64
|
+
print(f" Pro-ker Proteomics Analysis v{ver} installed")
|
|
65
|
+
print(f" {'=' * 44}")
|
|
66
|
+
print()
|
|
67
|
+
if not is_upgrade:
|
|
68
|
+
print(" Supported formats:")
|
|
69
|
+
print(" MaxQuant proteinGroups.txt (tab-separated)")
|
|
70
|
+
print()
|
|
71
|
+
print(" Quick start:")
|
|
72
|
+
print(" - Upload a proteinGroups.txt file")
|
|
73
|
+
print(" - Assign samples to groups")
|
|
74
|
+
print(" - Configure processing options")
|
|
75
|
+
print(" - Add visualizations to the canvas")
|
|
76
|
+
print()
|
|
77
|
+
print(" Commands:")
|
|
78
|
+
print(" proker Launch the viewer")
|
|
79
|
+
print(" proker file.txt Load a file on start")
|
|
80
|
+
print(" proker --update Check for updates")
|
|
81
|
+
print(" proker --install Create a desktop shortcut")
|
|
82
|
+
print(" proker --version Show installed version")
|
|
83
|
+
print()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ── Auto-update ──────────────────────────────────────────────────
|
|
87
|
+
def _get_installed_version():
|
|
88
|
+
try:
|
|
89
|
+
from proteomicsviewer import __version__
|
|
90
|
+
return __version__
|
|
91
|
+
except Exception:
|
|
92
|
+
return "0.0.0"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _get_pypi_version():
|
|
96
|
+
try:
|
|
97
|
+
import urllib.request
|
|
98
|
+
url = "https://pypi.org/pypi/proker/json"
|
|
99
|
+
with urllib.request.urlopen(url, timeout=3) as resp:
|
|
100
|
+
data = json.loads(resp.read())
|
|
101
|
+
return data["info"]["version"]
|
|
102
|
+
except Exception:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _version_tuple(v):
|
|
107
|
+
try:
|
|
108
|
+
return tuple(int(x) for x in v.split(".")[:3])
|
|
109
|
+
except Exception:
|
|
110
|
+
return (0, 0, 0)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _should_check_update():
|
|
114
|
+
try:
|
|
115
|
+
if _UPDATE_CHECK_FILE.exists():
|
|
116
|
+
last = float(_UPDATE_CHECK_FILE.read_text().strip())
|
|
117
|
+
return (time.time() - last) > _UPDATE_CHECK_INTERVAL
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _record_update_check():
|
|
124
|
+
try:
|
|
125
|
+
_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
_UPDATE_CHECK_FILE.write_text(str(time.time()))
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _do_upgrade():
|
|
132
|
+
cmds = [
|
|
133
|
+
[sys.executable, "-m", "pip", "install", "--upgrade", "proker"],
|
|
134
|
+
[sys.executable, "-m", "pip", "install", "--upgrade", "--user", "proker"],
|
|
135
|
+
]
|
|
136
|
+
last_err = None
|
|
137
|
+
for cmd in cmds:
|
|
138
|
+
try:
|
|
139
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
140
|
+
if result.returncode == 0:
|
|
141
|
+
return True
|
|
142
|
+
last_err = result.stderr.strip()[:200]
|
|
143
|
+
except Exception as e:
|
|
144
|
+
last_err = str(e)
|
|
145
|
+
if last_err:
|
|
146
|
+
_log(f" pip failed: {last_err}")
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _log(msg):
|
|
151
|
+
try:
|
|
152
|
+
print(msg)
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
try:
|
|
156
|
+
log = _CONFIG_DIR / "update.log"
|
|
157
|
+
_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
with open(log, "a") as f:
|
|
159
|
+
f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {msg}\n")
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def check_and_update(force=False):
|
|
165
|
+
if not force and not _should_check_update():
|
|
166
|
+
return ('skip', None)
|
|
167
|
+
|
|
168
|
+
_record_update_check()
|
|
169
|
+
installed = _get_installed_version()
|
|
170
|
+
latest = _get_pypi_version()
|
|
171
|
+
|
|
172
|
+
if latest is None:
|
|
173
|
+
return ('skip', None)
|
|
174
|
+
if _version_tuple(latest) <= _version_tuple(installed):
|
|
175
|
+
return ('current', None)
|
|
176
|
+
|
|
177
|
+
_log(f" Updating Pro-ker: {installed} -> {latest} ...")
|
|
178
|
+
if _do_upgrade():
|
|
179
|
+
_log(f" Updated to {latest}.")
|
|
180
|
+
return ('updated', latest)
|
|
181
|
+
else:
|
|
182
|
+
_log(f" Update failed (retry with: proker --update)")
|
|
183
|
+
return ('failed', latest)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _check_update_with_prompt():
|
|
187
|
+
try:
|
|
188
|
+
_record_update_check()
|
|
189
|
+
installed = _get_installed_version()
|
|
190
|
+
latest = _get_pypi_version()
|
|
191
|
+
|
|
192
|
+
if latest is None or _version_tuple(latest) <= _version_tuple(installed):
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
has_terminal = False
|
|
196
|
+
try:
|
|
197
|
+
if sys.stdout is not None and hasattr(sys.stdout, 'write'):
|
|
198
|
+
sys.stdout.write('')
|
|
199
|
+
has_terminal = True
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
should_update = False
|
|
204
|
+
|
|
205
|
+
if has_terminal:
|
|
206
|
+
try:
|
|
207
|
+
print(f"\n Update available: {installed} -> {latest}")
|
|
208
|
+
answer = input(" Install update now? [Y/n]: ").strip()
|
|
209
|
+
should_update = answer.lower() != 'n'
|
|
210
|
+
except (EOFError, OSError):
|
|
211
|
+
should_update = _tk_update_prompt(installed, latest)
|
|
212
|
+
else:
|
|
213
|
+
should_update = _tk_update_prompt(installed, latest)
|
|
214
|
+
|
|
215
|
+
if should_update:
|
|
216
|
+
_log(f" Updating Pro-ker: {installed} -> {latest} ...")
|
|
217
|
+
if _do_upgrade():
|
|
218
|
+
_log(f" Updated to {latest}.")
|
|
219
|
+
if has_terminal:
|
|
220
|
+
print(f"\n Please run 'proker' again to launch the new version.\n")
|
|
221
|
+
sys.exit(0)
|
|
222
|
+
else:
|
|
223
|
+
try:
|
|
224
|
+
import tkinter as tk
|
|
225
|
+
from tkinter import messagebox
|
|
226
|
+
root = tk.Tk(); root.withdraw()
|
|
227
|
+
messagebox.showinfo(
|
|
228
|
+
"Pro-ker",
|
|
229
|
+
f"Updated to v{latest}.\n\nThe viewer will now relaunch."
|
|
230
|
+
)
|
|
231
|
+
root.destroy()
|
|
232
|
+
except Exception:
|
|
233
|
+
pass
|
|
234
|
+
subprocess.Popen([sys.executable, "-m", "proteomicsviewer", "--no-update"])
|
|
235
|
+
sys.exit(0)
|
|
236
|
+
else:
|
|
237
|
+
_log(f" Update failed. Retry with: proker --update")
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _tk_update_prompt(installed, latest):
|
|
243
|
+
try:
|
|
244
|
+
import tkinter as tk
|
|
245
|
+
from tkinter import messagebox
|
|
246
|
+
root = tk.Tk(); root.withdraw()
|
|
247
|
+
answer = messagebox.askyesno(
|
|
248
|
+
"Pro-ker Update",
|
|
249
|
+
f"A new version is available: {installed} -> {latest}\n\n"
|
|
250
|
+
f"Install the update now?",
|
|
251
|
+
)
|
|
252
|
+
root.destroy()
|
|
253
|
+
return answer
|
|
254
|
+
except Exception:
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ── Instance detection ───────────────────────────────────────────
|
|
259
|
+
def _pid_alive(pid):
|
|
260
|
+
if sys.platform == "win32":
|
|
261
|
+
import ctypes
|
|
262
|
+
kernel32 = ctypes.windll.kernel32
|
|
263
|
+
handle = kernel32.OpenProcess(0x100000, False, pid)
|
|
264
|
+
if handle:
|
|
265
|
+
kernel32.CloseHandle(handle)
|
|
266
|
+
return True
|
|
267
|
+
return False
|
|
268
|
+
try:
|
|
269
|
+
os.kill(pid, 0)
|
|
270
|
+
return True
|
|
271
|
+
except (OSError, ProcessLookupError, PermissionError):
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _http_check(host, port, timeout=0.5):
|
|
276
|
+
try:
|
|
277
|
+
import urllib.request
|
|
278
|
+
url = f"http://{'localhost' if host in ('0.0.0.0', '127.0.0.1') else host}:{port}/health"
|
|
279
|
+
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
|
280
|
+
return json.loads(resp.read()).get("status") == "ok"
|
|
281
|
+
except Exception:
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _check_existing_server(host, port):
|
|
286
|
+
try:
|
|
287
|
+
if _LOCK_FILE.exists():
|
|
288
|
+
data = json.loads(_LOCK_FILE.read_text())
|
|
289
|
+
lock_host = data.get("host", "127.0.0.1")
|
|
290
|
+
lock_port = data.get("port", 8050)
|
|
291
|
+
lock_pid = data.get("pid")
|
|
292
|
+
if lock_pid and _pid_alive(lock_pid) and _http_check(lock_host, lock_port):
|
|
293
|
+
h = "localhost" if lock_host in ("0.0.0.0", "127.0.0.1") else lock_host
|
|
294
|
+
return f"http://{h}:{lock_port}"
|
|
295
|
+
_LOCK_FILE.unlink(missing_ok=True)
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
if _http_check(host, port):
|
|
300
|
+
h = "localhost" if host in ("0.0.0.0", "127.0.0.1") else host
|
|
301
|
+
return f"http://{h}:{port}"
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ── Lock file helpers ────────────────────────────────────────────
|
|
306
|
+
def _write_lock(host, port):
|
|
307
|
+
try:
|
|
308
|
+
_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
309
|
+
_LOCK_FILE.write_text(json.dumps({"host": host, "port": port, "pid": os.getpid()}))
|
|
310
|
+
except Exception:
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _remove_lock():
|
|
315
|
+
try:
|
|
316
|
+
if _LOCK_FILE.exists():
|
|
317
|
+
data = json.loads(_LOCK_FILE.read_text())
|
|
318
|
+
if data.get("pid") == os.getpid():
|
|
319
|
+
_LOCK_FILE.unlink()
|
|
320
|
+
except Exception:
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ── First-run shortcut prompt ────────────────────────────────────
|
|
325
|
+
def _offer_shortcut_install():
|
|
326
|
+
if _FIRST_RUN_MARKER.exists():
|
|
327
|
+
return
|
|
328
|
+
try:
|
|
329
|
+
_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
330
|
+
_FIRST_RUN_MARKER.write_text("prompted")
|
|
331
|
+
except Exception:
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
answer = input(" Create a desktop shortcut? [Y/n]: ").strip()
|
|
336
|
+
if answer.lower() != 'n':
|
|
337
|
+
try:
|
|
338
|
+
from proteomicsviewer.install_shortcut import main as install_main
|
|
339
|
+
install_main()
|
|
340
|
+
except ImportError:
|
|
341
|
+
print(" Shortcut creation requires tkinter.")
|
|
342
|
+
print(" You can create a shortcut later with: proker --install")
|
|
343
|
+
except Exception as e:
|
|
344
|
+
print(f" Shortcut creation failed: {e}")
|
|
345
|
+
print(" You can try again later with: proker --install")
|
|
346
|
+
return
|
|
347
|
+
except (EOFError, OSError):
|
|
348
|
+
pass
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
import tkinter as tk
|
|
352
|
+
from tkinter import messagebox
|
|
353
|
+
root = tk.Tk(); root.withdraw()
|
|
354
|
+
answer = messagebox.askyesno(
|
|
355
|
+
"Pro-ker",
|
|
356
|
+
"Would you like to create a desktop shortcut?\n\n"
|
|
357
|
+
"You can also do this later with: proker --install",
|
|
358
|
+
)
|
|
359
|
+
root.destroy()
|
|
360
|
+
if answer:
|
|
361
|
+
from proteomicsviewer.install_shortcut import main as install_main
|
|
362
|
+
install_main()
|
|
363
|
+
except Exception:
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ── Browser opener ───────────────────────────────────────────────
|
|
368
|
+
def _open_when_ready(url, timeout=10):
|
|
369
|
+
import urllib.request
|
|
370
|
+
deadline = time.monotonic() + timeout
|
|
371
|
+
while time.monotonic() < deadline:
|
|
372
|
+
try:
|
|
373
|
+
urllib.request.urlopen(f"{url}/health", timeout=0.5)
|
|
374
|
+
break
|
|
375
|
+
except Exception:
|
|
376
|
+
time.sleep(0.2)
|
|
377
|
+
webbrowser.open(url)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ── Entry point ──────────────────────────────────────────────────
|
|
381
|
+
def main():
|
|
382
|
+
parser = argparse.ArgumentParser(
|
|
383
|
+
prog="proker",
|
|
384
|
+
description="Pro-ker Proteomics Analysis — interactive proteomics data visualization",
|
|
385
|
+
)
|
|
386
|
+
parser.add_argument("file", nargs="?", default=None,
|
|
387
|
+
help="Path to a proteinGroups.txt file to auto-load")
|
|
388
|
+
parser.add_argument("--port", type=int, default=8050, help="Port (default: 8050)")
|
|
389
|
+
parser.add_argument("--host", type=str, default="127.0.0.1", help="Host (default: 127.0.0.1)")
|
|
390
|
+
parser.add_argument("--no-browser", action="store_true", help="Don't open the browser")
|
|
391
|
+
parser.add_argument("--install", action="store_true", help="Create a desktop shortcut")
|
|
392
|
+
parser.add_argument("--update", action="store_true", help="Check for updates now")
|
|
393
|
+
parser.add_argument("--no-update", action="store_true", help="Skip automatic update check")
|
|
394
|
+
parser.add_argument("--version", action="store_true", help="Show version and exit")
|
|
395
|
+
args = parser.parse_args()
|
|
396
|
+
|
|
397
|
+
if args.version:
|
|
398
|
+
print(f"Pro-ker Proteomics Analysis {_get_installed_version()}")
|
|
399
|
+
return 0
|
|
400
|
+
|
|
401
|
+
if args.install:
|
|
402
|
+
try:
|
|
403
|
+
from proteomicsviewer.install_shortcut import main as install_main
|
|
404
|
+
install_main()
|
|
405
|
+
except ImportError:
|
|
406
|
+
print(" Shortcut creation requires tkinter.")
|
|
407
|
+
print(" Install it with: conda install tk (or) sudo apt install python3-tk")
|
|
408
|
+
except Exception as e:
|
|
409
|
+
print(f" Shortcut creation failed: {e}")
|
|
410
|
+
return 0
|
|
411
|
+
|
|
412
|
+
# Manual update command
|
|
413
|
+
if args.update:
|
|
414
|
+
status, ver = check_and_update(force=True)
|
|
415
|
+
if status == 'updated':
|
|
416
|
+
print(f" Pro-ker updated to {ver}.")
|
|
417
|
+
print(f" Run 'proker' to launch the new version.")
|
|
418
|
+
elif status == 'failed':
|
|
419
|
+
print(f" Update to {ver} failed.")
|
|
420
|
+
print(f" Check ~/.proker/update.log for details.")
|
|
421
|
+
print(f" Or update manually: pip install --upgrade proker")
|
|
422
|
+
elif status == 'skip':
|
|
423
|
+
print(f" Could not reach PyPI. Check your internet connection.")
|
|
424
|
+
else:
|
|
425
|
+
print(f" Pro-ker {_get_installed_version()} is up to date.")
|
|
426
|
+
return 0
|
|
427
|
+
|
|
428
|
+
# Welcome message on first run / upgrade
|
|
429
|
+
_show_welcome_if_new()
|
|
430
|
+
|
|
431
|
+
# Pre-launch update check
|
|
432
|
+
has_terminal = False
|
|
433
|
+
try:
|
|
434
|
+
if sys.stdout is not None and hasattr(sys.stdout, 'write'):
|
|
435
|
+
sys.stdout.write('')
|
|
436
|
+
has_terminal = True
|
|
437
|
+
except Exception:
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
if not args.no_update and has_terminal:
|
|
441
|
+
_check_update_with_prompt()
|
|
442
|
+
|
|
443
|
+
# Single-instance check
|
|
444
|
+
existing = _check_existing_server(args.host, args.port)
|
|
445
|
+
if existing:
|
|
446
|
+
if not args.no_browser:
|
|
447
|
+
webbrowser.open(existing)
|
|
448
|
+
return 0
|
|
449
|
+
|
|
450
|
+
# First launch — offer desktop shortcut
|
|
451
|
+
_offer_shortcut_install()
|
|
452
|
+
|
|
453
|
+
# Store file path for auto-load
|
|
454
|
+
if args.file:
|
|
455
|
+
filepath = Path(args.file).resolve()
|
|
456
|
+
if not filepath.exists():
|
|
457
|
+
print(f"Error: file not found: {filepath}", file=sys.stderr)
|
|
458
|
+
return 1
|
|
459
|
+
os.environ["PROTVIEW_AUTOLOAD"] = str(filepath)
|
|
460
|
+
|
|
461
|
+
url = f"http://{'localhost' if args.host in ('0.0.0.0',) else args.host}:{args.port}"
|
|
462
|
+
|
|
463
|
+
if not args.no_browser:
|
|
464
|
+
threading.Thread(target=_open_when_ready, args=(url,), daemon=True).start()
|
|
465
|
+
|
|
466
|
+
# Heavy imports
|
|
467
|
+
backend_dir = os.path.join(os.path.dirname(__file__), "server")
|
|
468
|
+
sys.path.insert(0, backend_dir)
|
|
469
|
+
os.environ["PROTVIEW_AUTO_SHUTDOWN"] = "1"
|
|
470
|
+
|
|
471
|
+
from proteomicsviewer.server.main import app # noqa: E402
|
|
472
|
+
|
|
473
|
+
_write_lock(args.host, args.port)
|
|
474
|
+
atexit.register(_remove_lock)
|
|
475
|
+
|
|
476
|
+
# Background update check for pythonw launches
|
|
477
|
+
if not args.no_update and not has_terminal:
|
|
478
|
+
def _bg_update_check():
|
|
479
|
+
time.sleep(3)
|
|
480
|
+
try:
|
|
481
|
+
installed = _get_installed_version()
|
|
482
|
+
latest = _get_pypi_version()
|
|
483
|
+
if latest and _version_tuple(latest) > _version_tuple(installed):
|
|
484
|
+
_log(f" Update available: {installed} -> {latest}. Run 'proker --update'.")
|
|
485
|
+
if _tk_update_prompt(installed, latest):
|
|
486
|
+
if _do_upgrade():
|
|
487
|
+
_log(f" Updated to {latest}.")
|
|
488
|
+
try:
|
|
489
|
+
import tkinter as tk
|
|
490
|
+
from tkinter import messagebox
|
|
491
|
+
root = tk.Tk(); root.withdraw()
|
|
492
|
+
messagebox.showinfo("Pro-ker", f"Updated to v{latest}.\n\nRestart to use the new version.")
|
|
493
|
+
root.destroy()
|
|
494
|
+
except Exception:
|
|
495
|
+
pass
|
|
496
|
+
except Exception:
|
|
497
|
+
pass
|
|
498
|
+
threading.Thread(target=_bg_update_check, daemon=True).start()
|
|
499
|
+
|
|
500
|
+
# Start server
|
|
501
|
+
if sys.platform == "win32":
|
|
502
|
+
import asyncio
|
|
503
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
504
|
+
|
|
505
|
+
import uvicorn
|
|
506
|
+
uvicorn.run(app, host=args.host, port=args.port, log_level="warning")
|
|
507
|
+
return 0
|