Compare commits

...
3 Commits
Author SHA1 Message Date
Ross 66b4fffb7b tidy up 2024-12-09 11:13:46 +00:00
Ross 8aae29f3e8 Merge branch 'main' of https://git.xkjq.uk/ross/uploader 2024-12-09 09:26:45 +00:00
Ross 15f2e4a6e4 add typer 2024-12-09 09:26:12 +00:00
10 changed files with 248 additions and 553 deletions
+3 -1
View File
@@ -11,4 +11,6 @@ dist
test/* test/*
*.build *.build
C:/* # Windows paths on linux
-44
View File
@@ -1,44 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['anon_gui.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='anon_gui',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
-6
View File
@@ -1,6 +0,0 @@
@echo off
rem This script was created by Nuitka to execute 'anon_gui.exe' with Python DLL being found.
set PATH=C:\PROGRA~1\PYTHON~1;%PATH%
set PYTHONHOME=C:\Program Files\Python311
"%~dp0anon_gui.exe" %*
BIN
View File
Binary file not shown.
+3
View File
@@ -1,3 +1,6 @@
# This is a legacy app written in wxPython
# Whilst it works (or did prior to any API changes)
# it was not particularly well designed
from collections import defaultdict from collections import defaultdict
import json import json
import os import os
-65
View File
@@ -1,65 +0,0 @@
import typer
import requests
app = typer.Typer()
from bs4 import BeautifulSoup
# client.py
LOGIN_URL = "https://www.penracourses.org.uk/accounts/login"
# ADD_URL = 'http://localhost/add'
@app.command()
def login(user: str, password: str):
rqst = requests.session()
rsp = rqst.get(LOGIN_URL)
token = BeautifulSoup(rsp.content, "html.parser").find("input").attrs["value"]#, attr={"name": "csrfmiddlewaretoken"}).attrs("value")
#token = rsp.cookies["csrftoken"]
#header = {"X-CSRFToken": token}
#cookies = {"csrftoken": token}
data = {
"username": user,
"password": password,
"csrfmiddlewaretoken": token,
"next": "/",
}
print(data)
rqst.headers["Referer"] = LOGIN_URL
rsp = rqst.post(
LOGIN_URL,
data=data,
#headers=header,
#cookies=cookies
)
soup = BeautifulSoup(rsp.content, "html.parser")
print(token)
print(rqst.headers)
print(rqst.cookies)
login_success = False
if soup.find("button") and soup.find("button").find(string="Login"):
print("login fail")
pass
else:
print("login success")
login_success = True
#print(rqst.headers)
#print(rsp.content)
#new = rqst.get(
# "https://www.penracourses.org.uk/api/atlas/get_cases_user"
#)
#print(new)
#print(new.content)
if __name__ == "__main__":
app()
+242 -215
View File
@@ -1,20 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import asyncio import asyncio
import pynng import pynng
#asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from local_file_picker import local_file_picker
from nicegui import ui, app, html, run, Client, native from nicegui import ui, app, html, run, Client, native
from datetime import datetime from datetime import datetime
import zmq
import zmq.asyncio
from loguru import logger from loguru import logger
import sys import sys
@@ -26,10 +23,9 @@ import time
from multiprocessing import Manager, Queue, freeze_support from multiprocessing import Manager, Queue, freeze_support
import watchfiles
from functools import partial from functools import partial
from local_file_picker import local_file_picker
from local_folder_picker import local_folder_picker from local_folder_picker import local_folder_picker
from typing import Optional from typing import Optional
@@ -37,35 +33,15 @@ from typing import Optional
from fastapi import Request from fastapi import Request
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
#import typer import typer
#typer_app = typer.Typer() SOCKET_PATH = "tcp://localhost:9976"
#logger.add(sys.stderr, format="{time} {level} {message}", filter="my_module", level="INFO") WORK_DIR = Path("C:/uploader")
#async def pick_file() -> None: EXPORT_PATH = WORK_DIR / Path("export/")
# result = await local_file_picker('~', multiple=True) PROCESS_PATH = WORK_DIR / Path("to_process/")
# ui.notify(f'You chose {result}') ANON_PATH = WORK_DIR / Path("anon/")
# ui.html("<h2>Uploader tool</h2>")
#
# ui.chat_message(f'{result}!',
# name='Robot',
# stamp='now',
# avatar='https://robohash.org/ui')
#
#
#@ui.page('/')
#def index():
# ui.html("<h2>Uploader tool</h2>")
# ui.button('Choose file', on_click=pick_file, icon='folder')
NATIVE = False
SOCKET_PATH = 'tcp://localhost:9976'
EXPORT_PATH = Path("C:/uploader/export")
PROCESS_PATH = Path("C:/uploader/to_process")
ANON_PATH = Path("C:/uploader/anon")
EXPORT_PATH.mkdir(exist_ok=True, parents=True) EXPORT_PATH.mkdir(exist_ok=True, parents=True)
PROCESS_PATH.mkdir(exist_ok=True, parents=True) PROCESS_PATH.mkdir(exist_ok=True, parents=True)
@@ -81,6 +57,7 @@ LOGGED_IN_USER = "None"
REFRESH_TRIGGERED = True REFRESH_TRIGGERED = True
SHUTDOWN = False
DELETE_FILES_ON_UPLOAD = True DELETE_FILES_ON_UPLOAD = True
@@ -96,8 +73,6 @@ if DEBUG:
rqst = None rqst = None
use_zmq = False
use_nng = True
loaded_files: dict = {} loaded_files: dict = {}
loaded_series = defaultdict(list) loaded_series = defaultdict(list)
loaded_series_data = {} loaded_series_data = {}
@@ -105,88 +80,44 @@ uploaded_files: dict = {}
duplicate_series = set() duplicate_series = set()
async def read_nng_messages():
if use_zmq: print("start nng")
context = zmq.asyncio.Context()
socket = context.socket(zmq.PULL)
#socket.bind('tcp://localhost:5575')
try: try:
socket.bind(SOCKET_PATH) with pynng.Pair0(listen=SOCKET_PATH) as sub:
except zmq.error.ZMQError as e: # sub.subscribe('')
logger.debug("send reload") while not app.is_stopped:
socket = context.socket(zmq.PUSH) msg = await sub.arecv_msg()
socket.connect(SOCKET_PATH) data = msg.bytes.decode()
socket.send_string("loaded") logger.debug(f"Received msg {data}")
sys.exit(0) match data:
#app.shutdown()
poller = zmq.asyncio.Poller()
poller.register(socket, zmq.POLLIN)
async def read_loop() -> None:
while not app.is_stopped:
events = await poller.poll()
if socket in dict(events):
data = await socket.recv()
msg = data.decode()
logger.debug(f'Received msg {msg}')
match msg:
case "loaded": case "loaded":
for client in Client.instances.values(): for client in Client.instances.values():
if not client.has_socket_connection: if not client.has_socket_connection:
continue continue
with client: with client:
ui.notify("Export done", color='positive') ui.notify("Export done", color="positive")
global REFRESH_TRIGGERED global REFRESH_TRIGGERED
REFRESH_TRIGGERED = True REFRESH_TRIGGERED = True
logger.debug("loaded") logger.debug("loaded")
#with updates:
# ui.item(f"{datetime.now().strftime('%H:%M:%S')} - {msg}")
#logger.debug("loaded")
#load_files_start()
case _: case _:
logger.error("invalid message") logger.error("invalid message")
except pynng.exceptions.AddressInUse:
logger.debug("Address in use")
with pynng.Pair0(dial=SOCKET_PATH) as sub:
await sub.asend(b"loaded")
if use_nng: # Trigger shutdown from in the async loop
async def read_nng_messages(): # This shouldn't be necessary
print("start nng") global SHUTDOWN
try: SHUTDOWN = True
with pynng.Pair0(listen=SOCKET_PATH) as sub: return
#sub.subscribe('')
while not app.is_stopped:
msg = await sub.arecv_msg()
data = msg.bytes.decode()
logger.debug(f'Received msg {data}')
match data:
case "loaded":
for client in Client.instances.values():
if not client.has_socket_connection:
continue
with client:
ui.notify("Export done", color='positive')
global REFRESH_TRIGGERED
REFRESH_TRIGGERED = True
logger.debug("loaded")
case _:
logger.error("invalid message")
except pynng.exceptions.AddressInUse:
logger.debug("Address in use")
with pynng.Pair0(dial=SOCKET_PATH) as sub:
await sub.asend(b"loaded")
sys.exit(0)
#asyncio.create_task(read_nng_messages())
# asyncio.create_task(read_nng_messages())
anonymizer = Anonymizer(ANON_PATH) anonymizer = Anonymizer(ANON_PATH)
async def upload_files_start(progress, upload_queue): async def upload_files_start(progress, upload_queue):
global uploaded_files, duplicate_series global uploaded_files, duplicate_series
progress.visible = True progress.visible = True
@@ -195,67 +126,91 @@ async def upload_files_start(progress, upload_queue):
progress.visible = False progress.visible = False
if results: if results:
new_uploaded_files, upload_file_list, duplicate_file_list, failed, duplicate_series = results (
new_uploaded_files,
upload_file_list,
duplicate_file_list,
failed,
duplicate_series,
) = results
else: else:
ui.notify("No files to upload", color="negative") ui.notify("No files to upload", color="negative")
return return
uploaded_files.update(new_uploaded_files) uploaded_files.update(new_uploaded_files)
ui.notify(f"Uploaded {len(upload_file_list)} files", color='positive') ui.notify(f"Uploaded {len(upload_file_list)} files", color="positive")
ui.notify(f"Duplicate {len(duplicate_file_list)} files", color='warning')
ui.notify(f"Failed {len(failed)} files", color='negative') if duplicate_file_list:
ui.notify(f"Duplicate {len(duplicate_file_list)} files", color="warning")
if failed:
ui.notify(f"Failed {len(failed)} files", color="negative")
if DELETE_FILES_ON_UPLOAD: if DELETE_FILES_ON_UPLOAD:
clear_anonymized_files() clear_anonymized_files()
loaded_series_ui.refresh() loaded_series_ui.refresh()
loaded_files_ui.refresh() loaded_files_ui.refresh()
uploaded_files_ui.refresh() uploaded_files_ui.refresh()
async def load_files_start(progress_bar, queue, custom_path=None): async def load_files_start(progress_bar, queue, custom_path=None):
logger.debug("load files start") logger.debug("load files start")
global loaded_files, loaded_series, loaded_series_data global loaded_files, loaded_series, loaded_series_data
progress_bar.visible = True progress_bar.visible = True
new_loaded_files, new_loaded_series, new_loaded_series_data = await run.cpu_bound(load_files, queue, custom_path) new_loaded_files, new_loaded_series, new_loaded_series_data = await run.cpu_bound(
load_files, queue, custom_path
)
for client in Client.instances.values():
if not client.has_socket_connection:
continue
with client:
ui.notify(f"Files loaded: {len(new_loaded_files)}, [Series: {len(new_loaded_series)}] ", color="positive")
loaded_files.update(new_loaded_files) loaded_files.update(new_loaded_files)
loaded_series.update(new_loaded_series) loaded_series.update(new_loaded_series)
loaded_series_data.update(new_loaded_series_data) loaded_series_data.update(new_loaded_series_data)
progress_bar.visible = False progress_bar.visible = False
#load_series_view(loaded_series, loaded_series_data) # load_series_view(loaded_series, loaded_series_data)
loaded_series_ui.refresh() loaded_series_ui.refresh()
loaded_files_ui.refresh() loaded_files_ui.refresh()
async def reload_anonymized_start(progress_bar, queue): async def reload_anonymized_start(progress_bar, queue):
global loaded_files, loaded_series, loaded_series_data global loaded_files, loaded_series, loaded_series_data
progress_bar.visible = True progress_bar.visible = True
loaded_files, loaded_series, loaded_series_data = await run.cpu_bound(reload_anonymized, queue) loaded_files, loaded_series, loaded_series_data = await run.cpu_bound(
reload_anonymized, queue
)
progress_bar.visible = False progress_bar.visible = False
#load_series_view(loaded_series, loaded_series_data)
ui.notify(f"Files reloaded: {len(loaded_files)}, [Series: {len(loaded_series)}] ", color="positive")
# load_series_view(loaded_series, loaded_series_data)
loaded_series_ui.refresh() loaded_series_ui.refresh()
loaded_files_ui.refresh() loaded_files_ui.refresh()
@ui.refreshable @ui.refreshable
def user_info_ui(): def user_info_ui():
global LOGGED_IN_USER global LOGGED_IN_USER
user_label = ui.label(f"User: {LOGGED_IN_USER}") user_label = ui.label(f"User: {LOGGED_IN_USER}")
#user_label.bind_visibility_from(globals(), "LOGIN_SUCCESS") # user_label.bind_visibility_from(globals(), "LOGIN_SUCCESS")
@ui.refreshable @ui.refreshable
def loaded_series_ui() -> None: def loaded_series_ui() -> None:
global loaded_series, loaded_series_data global loaded_series, loaded_series_data
series_title = ui.label('Series to upload').classes("text-h3") series_title = ui.label("Series to upload").classes("text-h3")
series_title.visible = False series_title.visible = False
series = ui.row() series = ui.row()
#def load_series_view(loaded_series, loaded_series_data): # def load_series_view(loaded_series, loaded_series_data):
logger.debug("load series view") logger.debug("load series view")
logger.debug(loaded_series) logger.debug(loaded_series)
for key in loaded_series: for key in loaded_series:
@@ -269,9 +224,10 @@ def loaded_series_ui() -> None:
if loaded_series: if loaded_series:
series_title.visible = True series_title.visible = True
@ui.refreshable @ui.refreshable
def loaded_files_ui() -> None: def loaded_files_ui() -> None:
with ui.expansion('Loaded files', icon='drive_folder_upload').classes('w-full'): with ui.expansion("Loaded files", icon="drive_folder_upload").classes("w-full"):
ui.label(f"Items: {len(loaded_files)}") ui.label(f"Items: {len(loaded_files)}")
with ui.list(): with ui.list():
for file, data in loaded_files.items(): for file, data in loaded_files.items():
@@ -280,15 +236,21 @@ def loaded_files_ui() -> None:
@ui.refreshable @ui.refreshable
def uploaded_files_ui() -> None: def uploaded_files_ui() -> None:
with ui.expansion('Uploaded files', icon='file_upload').classes('w-full'): with ui.expansion("Uploaded files", icon="file_upload").classes("w-full"):
ui.label(f"Items: {len(uploaded_files)}") ui.label(f"Items: {len(uploaded_files)}")
with ui.list(): with ui.list():
for series in duplicate_series: for series in duplicate_series:
ui.item(f"Duplicate series: {series}", on_click=lambda: ui.navigate.to(f'{BASE_SITE_URL}{series}', new_tab=True)).classes("text-red-500") ui.item(
f"Duplicate series: {series}",
on_click=lambda: ui.navigate.to(
f"{BASE_SITE_URL}{series}", new_tab=True
),
).classes("text-red-500")
with ui.list(): with ui.list():
for file, data in uploaded_files.items(): for file, data in uploaded_files.items():
ui.item(f"{file} - {data}") ui.item(f"{file} - {data}")
def load_files(q: Queue, src_path: Path | None = None): def load_files(q: Queue, src_path: Path | None = None):
loaded_files: dict = {} loaded_files: dict = {}
loaded_series = defaultdict(list) loaded_series = defaultdict(list)
@@ -309,20 +271,26 @@ def load_files(q: Queue, src_path: Path | None = None):
to_process_len = len(to_process) to_process_len = len(to_process)
for n, file in enumerate(to_process): for n, file in enumerate(to_process):
dataset, output_file = anonymizer.anonymize_file(file, remove_original=True) dataset, output_file = anonymizer.anonymize_file(file, remove_original=True)
#with processed_files: # with processed_files:
# ui.item(f"{datetime.now().strftime('%H:%M:%S')} - {file.name} -> {output_file.name}") # ui.item(f"{datetime.now().strftime('%H:%M:%S')} - {file.name} -> {output_file.name}")
loaded_files[output_file] = (dataset.StudyDescription, dataset.SeriesDescription, str(dataset.SeriesInstanceUID)) loaded_files[output_file] = (
dataset.StudyDescription,
dataset.SeriesDescription,
str(dataset.SeriesInstanceUID),
)
loaded_series[dataset.SeriesInstanceUID].append(output_file) loaded_series[dataset.SeriesInstanceUID].append(output_file)
loaded_series_data[dataset.SeriesInstanceUID] = (dataset.StudyDescription, dataset.SeriesDescription) loaded_series_data[dataset.SeriesInstanceUID] = (
dataset.StudyDescription,
dataset.SeriesDescription,
)
q.put_nowait((n + 1) / to_process_len) q.put_nowait((n + 1) / to_process_len)
#time.sleep(0.01) # time.sleep(0.01)
return loaded_files, loaded_series, loaded_series_data
return loaded_files, loaded_series, loaded_series_data
def reload_anonymized(q: Queue): def reload_anonymized(q: Queue):
@@ -330,33 +298,39 @@ def reload_anonymized(q: Queue):
loaded_series = defaultdict(list) loaded_series = defaultdict(list)
loaded_series_data = {} loaded_series_data = {}
#processed_files.clear() # processed_files.clear()
to_process = list(ANON_PATH.glob("**/*.dcm")) to_process = list(ANON_PATH.glob("**/*.dcm"))
to_process_len = len(to_process) to_process_len = len(to_process)
for n, file in enumerate(to_process): for n, file in enumerate(to_process):
with pydicom.dcmread(file) as dataset: with pydicom.dcmread(file) as dataset:
loaded_files[file] = (dataset.StudyDescription, dataset.SeriesDescription, str(dataset.SeriesInstanceUID)) loaded_files[file] = (
dataset.StudyDescription,
dataset.SeriesDescription,
str(dataset.SeriesInstanceUID),
)
loaded_series[dataset.SeriesInstanceUID].append(file) loaded_series[dataset.SeriesInstanceUID].append(file)
loaded_series_data[dataset.SeriesInstanceUID] = (dataset.StudyDescription, dataset.SeriesDescription) loaded_series_data[dataset.SeriesInstanceUID] = (
dataset.StudyDescription,
dataset.SeriesDescription,
)
#with processed_files: # with processed_files:
# ui.item(f"{datetime.now().strftime('%H:%M:%S')} - {file.name} -> Reloaded") # ui.item(f"{datetime.now().strftime('%H:%M:%S')} - {file.name} -> Reloaded")
q.put_nowait(f"{(n + 1) / to_process_len * 100:.0f}%") q.put_nowait(f"{(n + 1) / to_process_len * 100:.0f}%")
logger.debug(file) logger.debug(file)
#time.sleep(0.01) # time.sleep(0.01)
logger.debug("end reload") logger.debug("end reload")
logger.debug(loaded_files) logger.debug(loaded_files)
logger.debug(loaded_series) logger.debug(loaded_series)
logger.debug("-----") logger.debug("-----")
return loaded_files, loaded_series, loaded_series_data return loaded_files, loaded_series, loaded_series_data
def clear_anonymized_files(): def clear_anonymized_files():
for file in ANON_PATH.iterdir(): for file in ANON_PATH.iterdir():
@@ -367,8 +341,9 @@ def clear_anonymized_files():
return return
def upload_files(q, rqst): def upload_files(q, rqst):
#global rqst#, uploaded_files # global rqst#, uploaded_files
uploaded_files = {} uploaded_files = {}
@@ -385,7 +360,6 @@ def upload_files(q, rqst):
files_to_upload[i : i + n] for i in range(0, len(files_to_upload), n) files_to_upload[i : i + n] for i in range(0, len(files_to_upload), n)
] ]
upload_file_list = [] upload_file_list = []
duplicate_file_list = [] duplicate_file_list = []
failed = [] failed = []
@@ -445,35 +419,37 @@ def upload_files(q, rqst):
for f in failed: for f in failed:
uploaded_files[f] = "failed" uploaded_files[f] = "failed"
return (
return uploaded_files, upload_file_list, duplicate_file_list, failed, duplicate_series uploaded_files,
upload_file_list,
duplicate_file_list,
failed,
duplicate_series,
)
@ui.page("/login")
@ui.page('/login')
def login() -> Optional[RedirectResponse]: def login() -> Optional[RedirectResponse]:
dark = ui.dark_mode() dark = ui.dark_mode()
dark.enable() dark.enable()
def try_login() -> None: # local function to avoid passing username and password as arguments
logger.debug(username.value) def try_login() -> (
#if passwords.get(username.value) == password.value: None
# app.storage.user.update({'username': username.value, 'authenticated': True}) ): # local function to avoid passing username and password as arguments
# ui.navigate.to(app.storage.user.get('referrer_path', '/')) # go back to where the user wanted to go # Because I haven't gotten around to implementing a proper login system yet
#else: # we just log in via requests and store teh session in the global rqst variable
# ui.notify('Wrong username or password', color='negative')
user = username.value user = username.value
pw = password.value pw = password.value
global rqst, LOGIN_SUCCESS, LOGGED_IN_USER global rqst, LOGIN_SUCCESS, LOGGED_IN_USER
rqst = requests.session() rqst = requests.session()
try: try:
rsp = rqst.get(LOGIN_URL) rsp = rqst.get(LOGIN_URL)
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
ui.notify('Connection error!', color='negative') ui.notify("Connection error!", color="negative")
return return
token = ( token = (
@@ -512,116 +488,124 @@ def login() -> Optional[RedirectResponse]:
LOGIN_SUCCESS = True LOGIN_SUCCESS = True
LOGGED_IN_USER = user LOGGED_IN_USER = user
user_info_ui.refresh() user_info_ui.refresh()
#user_label.clear() # user_label.clear()
#user_label.text = f"User: {user}" # user_label.text = f"User: {user}"
# print(rqst.headers) # print(rqst.headers)
rqst.headers["X-CSRFToken"] = token rqst.headers["X-CSRFToken"] = token
print(rqst.headers)
print(rqst.cookies)
new = rqst.get(f"{BASE_SITE_URL}/accounts/api/atlas/get_cases_user")
if LOGIN_SUCCESS: if LOGIN_SUCCESS:
ui.notify('Logged in!', color='positive') ui.notify("Logged in!", color="positive")
ui.navigate.to("/") ui.navigate.to("/")
#login_button.visible = False
pass
#self.parent.upload_button.Enable()
#self.parent.login_button.Hide()
#self.parent.panel.Layout()
#self.Destroy()
# self.parent.login_button.SetBackgroundColour(wx.BLUE)
# self.parent.login_button.SetForegroundColour(wx.WHITE)
else: else:
ui.notify('Wrong username or password', color='negative') ui.notify("Wrong username or password", color="negative")
pass
#self.login_fail()
#if app.storage.user.get('authenticated', False): # if app.storage.user.get('authenticated', False):
# return RedirectResponse('/') # return RedirectResponse('/')
with ui.card().classes('absolute-center'): with ui.card().classes("absolute-center"):
username = ui.input('Username').on('keydown.enter', try_login) username = ui.input("Username").on("keydown.enter", try_login)
password = ui.input('Password', password=True, password_toggle_button=True).on('keydown.enter', try_login) password = ui.input("Password", password=True, password_toggle_button=True).on(
ui.button('Log in', on_click=try_login) "keydown.enter", try_login
)
with ui.row():
ui.button("Log in", on_click=try_login)
ui.button("Cancel", on_click=lambda: ui.navigate.to("/"), color="red")
return None return None
@ui.page('/')
def main_page():
@ui.page("/")
def main_page():
async def watch_for_shutdown():
global SHUTDOWN
while not app.is_stopped:
if SHUTDOWN:
logger.debug("Trigger app shutdown")
app.shutdown()
await asyncio.sleep(1)
asyncio.create_task(watch_for_shutdown())
dark = ui.dark_mode() dark = ui.dark_mode()
dark.enable() dark.enable()
ui.page_title('Uploader tool') ui.page_title("Uploader tool")
ui.label('Uploader tool').classes("text-h2") ui.label("Uploader tool").classes("text-h2")
user_info_ui() user_info_ui()
login_button = ui.button("LOGIN", on_click=lambda: ui.navigate.to("/login")) login_button = ui.button("LOGIN", on_click=lambda: ui.navigate.to("/login"))
login_button.bind_visibility_from(globals(), "LOGIN_SUCCESS", backward=lambda v: not v) login_button.bind_visibility_from(
globals(), "LOGIN_SUCCESS", backward=lambda v: not v
)
updates = ui.list() updates = ui.list()
loaded_series_ui() loaded_series_ui()
# typer_app()
# typer_app()
# Create a queue to communicate with the heavy computation process # Create a queue to communicate with the heavy computation process
queue = Manager().Queue() queue = Manager().Queue()
# Update the progress bar on the main process # Update the progress bar on the main process
ui.timer(0.1, callback=lambda: progressbar.set_value(queue.get() if not queue.empty() else progressbar.value)) ui.timer(
0.1,
callback=lambda: progressbar.set_value(
queue.get() if not queue.empty() else progressbar.value
),
)
# Create a queue to communicate with the heavy computation process # Create a queue to communicate with the heavy computation process
upload_queue = Manager().Queue() upload_queue = Manager().Queue()
# Update the progress bar on the main process # Update the progress bar on the main process
ui.timer(0.1, callback=lambda: upload_progressbar.set_value(upload_queue.get() if not upload_queue.empty() else upload_progressbar.value)) ui.timer(
0.1,
callback=lambda: upload_progressbar.set_value(
upload_queue.get() if not upload_queue.empty() else upload_progressbar.value
),
)
upload_progress = ui.row().classes("w-full place-content-center bg-blue-900") upload_progress = ui.row().classes("w-full place-content-center bg-blue-900")
with upload_progress: with upload_progress:
ui.label("Uploading files") ui.label("Uploading files")
upload_progressbar = ui.linear_progress(value=0).props('instant-feedback') upload_progressbar = ui.linear_progress(value=0).props("instant-feedback")
upload_progress.visible = False upload_progress.visible = False
upload_button = ui.button("Upload", on_click=partial(upload_files_start, upload_progress, upload_queue)) upload_button = ui.button(
"Upload", on_click=partial(upload_files_start, upload_progress, upload_queue)
)
upload_button.bind_visibility_from(globals(), "LOGIN_SUCCESS") upload_button.bind_visibility_from(globals(), "LOGIN_SUCCESS")
anon_progress = ui.row().classes("w-full place-content-center bg-blue-900") anon_progress = ui.row().classes("w-full place-content-center bg-blue-900")
with anon_progress: with anon_progress:
ui.label("Anonymizing files") ui.label("Anonymizing files")
progressbar = ui.linear_progress(value=0).props('instant-feedback') progressbar = ui.linear_progress(value=0).props("instant-feedback")
anon_progress.visible = False anon_progress.visible = False
async def load_files_from_folder() -> None: async def load_files_from_folder() -> None:
folder = await local_folder_picker("~") folder = await local_folder_picker("~")
logger.error(folder) logger.error(folder)
if folder is not None: if folder is not None:
ui.notify(f"Selected folder: {folder}") ui.notify(f"Selected folder: {folder}")
await load_files_start(anon_progress, queue, folder) await load_files_start(anon_progress, queue, folder)
#return dir # return dir
else: else:
ui.notify("No folder selected", color='negative') ui.notify("No folder selected", color="negative")
with ui.expansion('Extra', icon='build').classes('w-full'): with ui.expansion("Extra", icon="build").classes("w-full"):
ui.button("Load files", on_click=partial(load_files_start, anon_progress, queue)) ui.button(
"Load files", on_click=partial(load_files_start, anon_progress, queue)
)
ui.button("Load files from folder", on_click=load_files_from_folder) ui.button("Load files from folder", on_click=load_files_from_folder)
ui.button("Reload anon", on_click=partial(reload_anonymized_start, anon_progress, queue)) ui.button(
"Reload anon",
on_click=partial(reload_anonymized_start, anon_progress, queue),
)
with ui.expansion('File status', icon='view_list').classes('w-full'): with ui.expansion("File status", icon="view_list").classes("w-full"):
loaded_files_ui() loaded_files_ui()
uploaded_files_ui() uploaded_files_ui()
#with ui.expansion('Processed files!', icon='work').classes('w-full'): # with ui.expansion('Processed files!', icon='work').classes('w-full'):
# processed_files = ui.list() # processed_files = ui.list()
with ui.expansion("Settings", icon="settings").classes("w-full"): with ui.expansion("Settings", icon="settings").classes("w-full"):
@@ -629,13 +613,13 @@ def main_page():
ui.label(f"Export path: {EXPORT_PATH}") ui.label(f"Export path: {EXPORT_PATH}")
ui.label(f"Process path: {PROCESS_PATH}") ui.label(f"Process path: {PROCESS_PATH}")
ui.label(f"Anon path: {ANON_PATH}") ui.label(f"Anon path: {ANON_PATH}")
ui.button("Clear anon path", on_click=clear_anonymized_files).tooltip("Delete all files in ANON_PATH") ui.button("Clear anon path", on_click=clear_anonymized_files).tooltip(
"Delete all files in ANON_PATH"
)
ui.button("Shutdown", on_click=app.shutdown) def shutdown_app():
app.shutdown()
ui.run_javascript("window.close()")
ui.notify("Welcome to the uploader tool", color='positive')
async def watch_for_refresh(): async def watch_for_refresh():
global REFRESH_TRIGGERED global REFRESH_TRIGGERED
@@ -647,15 +631,58 @@ def main_page():
asyncio.create_task(watch_for_refresh()) asyncio.create_task(watch_for_refresh())
with ui.dialog() as about_dialog:
with ui.card().classes("absolute-center"):
ui.label("Uploader tool").classes("text-h2")
ui.label("A tool to help upload files to penracourses.org.uk")
ui.image("icon/icon1.png")
ui.label("Version: 0.1")
ui.label("Author: Ross Kruger")
ui.button("Close", on_click=about_dialog.close, icon="close", color="secondary")
with ui.footer():
with ui.row():
ui.button("Shutdown", on_click=shutdown_app).props("outline color=white")
ui.button("About", on_click=about_dialog.open).props("outline color=white")
def launch_app(
work_dir: Path = typer.Option(WORK_DIR, help="Working directory"),
nng: bool = typer.Option(True, help="Use nng"),
native_mode: bool = typer.Option(False, help="Use native mode"),
):
global WORK_DIR, EXPORT_PATH, PROCESS_PATH, ANON_PATH
WORK_DIR = work_dir
EXPORT_PATH = WORK_DIR / Path("export/")
PROCESS_PATH = WORK_DIR / Path("to_process/")
ANON_PATH = WORK_DIR / Path("anon/")
logger.debug(f"Work dir: {WORK_DIR}")
if nng:
try:
with pynng.Pair0(listen=SOCKET_PATH) as sub:
pass
except pynng.exceptions.AddressInUse:
logger.debug("Address in use")
with pynng.Pair0(dial=SOCKET_PATH) as sub:
sub.send(b"loaded")
sys.exit(0)
app.on_startup(read_nng_messages)
app.on_exception(logger.debug)
try:
ui.run(reload=False, show=True, port=native.find_open_port(), native=native_mode, favicon="icon/icon1.ico")
except Exception as e:
logger.error(e)
pass
if __name__ in {"__main__", "__mp_main__"}: if __name__ in {"__main__", "__mp_main__"}:
freeze_support() freeze_support()
if use_zmq: typer.run(launch_app)
app.on_startup(read_loop)
if use_nng:
app.on_startup(read_nng_messages)
ui.run(storage_secret="123456", reload=False, show=True, port=native.find_open_port(), native=NATIVE)
-6
View File
@@ -1,6 +0,0 @@
import pynng
with pynng.Pair0(listen='tcp://127.0.0.1:54321') as s1:
with pynng.Pair0(listen='tcp://127.0.0.1:54321') as s2:
pass
-193
View File
@@ -1,193 +0,0 @@
import webview
import time
import sys
from datauri import DataURI
from pathlib import Path
import pydicom
import dicognito.anonymizer
import typer
from typing import List
from watchdog.observers import Observer
# from watchdog.events import LoggingEventHandler
from watchdog.events import PatternMatchingEventHandler
anonymizer = dicognito.anonymizer.Anonymizer()
app = typer.Typer()
def anonymize_file(filepath):
try:
with pydicom.dcmread(filepath) as dataset:
anonymizer.anonymize(dataset)
p = Path(filepath)
new_filename = Path(p.parent, f"{p.stem}_anon{p.suffix}")
dataset.save_as(new_filename)
return new_filename
except:
# Cannot anonymise so just return the same path
return filepath
class PythonJavascriptApi:
def __init__(self):
self.cancel_heavy_stuff_flag = False
def init(self):
response = {"message": "Hello from Python {0}".format(sys.version)}
return response
def sayHelloTo(self, name):
response = {"message": "Hello {0}!".format(name)}
return response
def error(self):
raise Exception("This is a Python exception")
class Watcher:
def __init__(self, window, anonymise: bool) -> None:
self.window = window
self.anonymise = anonymise
def on_created(self, event):
print(f"hey, {event.src_path} has been created!")
path = event.src_path
if self.anonymise:
path = anonymize_file(path)
try:
file_uri = DataURI.from_file(path)
except FileNotFoundError:
print("File deleted...")
return
file_name = Path(path).name
self.injectFile(file_uri, file_name)
def on_deleted(self, event):
print(f"what the f**k! Someone deleted {event.src_path}!")
def on_modified(self, event):
print(f"hey buddy, {event.src_path} has been modified")
def on_moved(self, event):
print(f"ok ok ok, someone moved {event.src_path} to {event.dest_path}")
def injectFile(self, file_uri: DataURI, file_name: str):
current_url = webview.windows[0].get_current_url()
if current_url == "https://www.penracourses.org.uk/rapids/create/":
print(f"inject {file_name}")
self.window.evaluate_js(
f"""
function urltoFile(url, filename, mimeType){{
return (fetch(url)
.then(function(res){{ return res.arrayBuffer(); }})
.then(function(buf){{return new File([buf], filename,{{type:mimeType}});}})
);
}}
$(document).ready(() => {{
//Usage example:
urltoFile('{file_uri}', '{file_name}', '{file_uri.mimetype}')
.then(function(file){{
console.log(file);
//inputs = $("#rapid-form input[type=file][id^=id_images-]");
inputs = extendInputs(1);
console.log(inputs);
el = inputs.get(0);
console.log(el);
let dT = new DataTransfer();
dT.clearData();
dT.items.add(file);
el.files = dT.files;
// Manually fire the change event (qt doesn't seem to...)
$(el).change();
}});
}});
"""
)
@app.command()
def anatomy(filepath: str):
raise NotImplementedError
@app.command()
def rapid(watch_path: Path = "/home/ross/watch", anonymise: bool = True):
api = PythonJavascriptApi()
window = webview.create_window(
"Upload Rapid", "https://www.penracourses.org.uk/rapids/create/", js_api=api
)
# def on_loaded():
# print('DOM is ready')
# current_url = webview.windows[0].get_current_url()
# if current_url == "https://www.penracourses.org.uk/rapids/create/":
# #window.evaluate_js("alert('tent')")
# for file_uri, file_name in uri_name_list:
# #time.sleep(2)
# ## unsubscribe event listener
# #webview.windows[0].loaded -= on_loaded
# #webview.windows[0].load_url('https://google.com')
# window.loaded += on_loaded
# window = upload_rapid(["/home/ross/Downloads/DICOM CT/DICOM CXR/STD1/SER1/CXR.dcm"])
patterns = ["*"]
ignore_patterns = ["*anon*"]
ignore_directories = False
case_sensitive = True
my_event_handler = PatternMatchingEventHandler(
patterns, ignore_patterns, ignore_directories, case_sensitive
)
watcher = Watcher(window, anonymise)
my_event_handler.on_created = watcher.on_created
my_event_handler.on_deleted = watcher.on_deleted
my_event_handler.on_modified = watcher.on_modified
my_event_handler.on_moved = watcher.on_moved
my_observer = Observer()
Path(watch_path).mkdir(parents=True, exist_ok=True)
my_observer.schedule(my_event_handler, watch_path, recursive=True)
my_observer.start()
# try:
# while True:
# time.sleep(1)
# except KeyboardInterrupt:
# my_observer.stop()
# my_observer.join()
webview.start(None, window, debug=True, gui="qt")
# f = "/media/ross/f6200b79-c343-4882-a245-28fc06d43006/Downloads/temp/breasts.jpg"
f = "/home/ross/Downloads/DICOM CT/DICOM CXR/STD1/SER1/CXR.dcm"
if __name__ == "__main__":
app()
-23
View File
@@ -1,23 +0,0 @@
#!/usr/bin/env python3
import asyncio
import random
import zmq
import zmq.asyncio
context = zmq.asyncio.Context()
socket = context.socket(zmq.PUSH)
socket.connect('tcp://localhost:5575')
async def send_loop():
#print(f'Sending number {number}')
print("Send hello")
await socket.send_string("loaded")
#await asyncio.sleep(0.1)
print("end")
asyncio.run(send_loop())