WIP, to be cleaned and merged
This commit is contained in:
parent
314adbeb1d
commit
d1e3152a83
30 changed files with 1498 additions and 248 deletions
39
benchmark.py
Normal file
39
benchmark.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
from datetime import datetime
|
||||
import time
|
||||
import json
|
||||
import statistics as stats
|
||||
from pprint import pprint
|
||||
import os
|
||||
import _ed_lrr
|
||||
|
||||
NUM_LOOPS=5
|
||||
|
||||
results={}
|
||||
|
||||
def time_run(w,s,file="benchmark.json", loops=None):
|
||||
global results,NUM_LOOPS
|
||||
if loops is None:
|
||||
loops=NUM_LOOPS
|
||||
for _ in range(loops):
|
||||
t_start = time.time()
|
||||
ret = _ed_lrr.route(s,48,None,'bfs',True,False,False,False,0.0,None,r"D:\devel\rust\ED_LRR\stars.csv",w,lambda *args,**kwargs: None)
|
||||
t_end = time.time()
|
||||
results.setdefault(w,[]).append({'ret':len(ret),'time':t_end - t_start})
|
||||
with open(file, "w") as of:
|
||||
json.dump(results, of,indent=2)
|
||||
|
||||
t_start = datetime.today()
|
||||
|
||||
for w in [1,2,4,7,8,0]:
|
||||
time_run(w,['Ix','72'])
|
||||
|
||||
print("Benchmark took:", datetime.today() - t_start)
|
||||
|
||||
|
||||
for workers,results in results.items():
|
||||
t_total=sum([res['time'] for res in results])/len(results)
|
||||
avg_len=sum([res['ret'] for res in results])/len(results)
|
||||
times.append([int(workers),timedelta(seconds=t_total),avg_len])
|
||||
|
||||
for k,v,l in sorted(times,key=lambda rec:rec[1]):
|
||||
print(k,v,l)
|
17
celery_rabbitmq_setup.ps1
Normal file
17
celery_rabbitmq_setup.ps1
Normal file
|
@ -0,0 +1,17 @@
|
|||
#RABBITMQ
|
||||
rabbitmqctl stop_app
|
||||
rabbitmqctl reset
|
||||
rabbitmqctl start_app
|
||||
rabbitmqctl add_user ed_lrr ed_lrr
|
||||
rabbitmqctl add_vhost ed_lrr
|
||||
rabbitmqctl set_user_tags ed_lrr ed_lrr
|
||||
rabbitmqctl set_permissions -p ed_lrr ed_lrr ".*" ".*" ".*"
|
||||
rabbitmqctl set_permissions guest ".*" ".*" ".*"
|
||||
rabbitmqctl set_permissions -p ed_lrr guest ".*" ".*" ".*"
|
||||
Write-Host RabbitMQ setup done
|
||||
#Celery
|
||||
$env:FORKED_BY_MULTIPROCESSING=1
|
||||
Write-Host starting Celery
|
||||
celery -A celery_test worker -E -l info
|
||||
|
||||
#celery -A celery_test flower --presistent --broker=pyamqp://ed_lrr:ed_lrr@localhost/ed_lrr --broker_api=http://ed_lrr:ed_lrr@localhost:15672/api/
|
9
celery_test.py
Normal file
9
celery_test.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from celery import Celery
|
||||
import _ed_lrr
|
||||
app = Celery('ed_lrr',backend = 'db+sqlite:///ed_lrr_results.sqlite', broker='pyamqp://ed_lrr:ed_lrr@localhost/ed_lrr')
|
||||
|
||||
@app.task(bind=True)
|
||||
def route(self,hops,jmp_range):
|
||||
def callback(state):
|
||||
self.update_state(state="PROGRESS", meta=state)
|
||||
return _ed_lrr.route(hops,jmp_range,None,'bfs',True,False,False,False,0.0,None,r"C:\Users\Earthnuker\AppData\Local\ED_LRR\data\stars.csv",0,callback)
|
18
celery_worker.py
Normal file
18
celery_worker.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
import time
|
||||
from celery_test import route
|
||||
|
||||
jobs = [route.delay(["Ix", "Colonia"], 48), route.delay(["Colonia", "Sol"], 48)]
|
||||
while True:
|
||||
for job in jobs:
|
||||
if job.ready():
|
||||
print([job, job.state, len(job.info)])
|
||||
else:
|
||||
print([job, job.state, job.info])
|
||||
print("="*10)
|
||||
time.sleep(1)
|
||||
|
||||
# 02c77491-9abd-4a88-ab2c-acdf2981086b
|
||||
# d56b0ca8-067d-45a6-be9b-bb9e74f196cd
|
||||
|
||||
# celery -A celery_test flower --presistent --broker=pyamqp://ed_lrr:ed_lrr@localhost/ed_lrr --broker_api=http://ed_lrr:ed_lrr@localhost:15672/api/
|
||||
|
|
@ -39,7 +39,7 @@ for file in cfg["history.bodies_path"][::-1]:
|
|||
@click.pass_context
|
||||
@click.version_option()
|
||||
def main(ctx):
|
||||
"Elite: Dangerous long range router, command line interface"
|
||||
"Elite: Dangerous long range router, command line interface."
|
||||
MP.freeze_support()
|
||||
if ctx.invoked_subcommand != "config":
|
||||
os.makedirs(cfg["folders.data_dir"], exist_ok=True)
|
||||
|
@ -49,12 +49,31 @@ def main(ctx):
|
|||
return
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option("--port", "-p", help="Port to bind to", type=int, default=3777)
|
||||
@click.option("--host", "-h", help="Address to bind to", type=str, default="0.0.0.0")
|
||||
@click.option("--debug", "-d", is_flag=True, help="Run using debug server")
|
||||
def web(host, port, debug):
|
||||
"Run web interface."
|
||||
from gevent.pywsgi import WSGIServer
|
||||
from ed_lrr_gui.web import app
|
||||
|
||||
with app.test_client() as c:
|
||||
c.get("/") # Force before_first_request hook to run
|
||||
if debug:
|
||||
app.debug=True
|
||||
app.run(host=host, port=port, debug=True)
|
||||
return
|
||||
print("Listening on {}:{}".format(host, port))
|
||||
server = WSGIServer((host, port), app)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument("option", default=None, required=False)
|
||||
@click.argument("value", default=None, required=False)
|
||||
def config(option, value):
|
||||
"""Change configuration
|
||||
|
||||
"""Change configuration.
|
||||
|
||||
If "key" and "value" are both omitted the current configuration is printed
|
||||
"""
|
||||
|
@ -98,14 +117,14 @@ def config(option, value):
|
|||
|
||||
@main.command()
|
||||
def explore():
|
||||
"Open file manager in data folder"
|
||||
"Open file manager in data folder."
|
||||
click.launch(cfg["folders.data_dir"], locate=True)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option("--debug", help="Debug print", is_flag=True)
|
||||
@click.option("--debug", help="Enable debug output", is_flag=True)
|
||||
def gui(debug):
|
||||
"Run the ED LRR GUI (default)"
|
||||
"Run the ED LRR GUI (default)."
|
||||
import ed_lrr_gui.gui as ED_LRR_GUI
|
||||
|
||||
if (not debug) and os.name == "nt":
|
||||
|
@ -133,7 +152,7 @@ def gui(debug):
|
|||
show_default=True,
|
||||
)
|
||||
def download(url, folder):
|
||||
"Download EDSM dumps"
|
||||
"Download EDSM dumps."
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
for file_name in ["systemsWithCoordinates.json", "bodies.json"]:
|
||||
download_url = urljoin(url, file_name)
|
||||
|
@ -156,6 +175,7 @@ def download(url, folder):
|
|||
unit_divisor=1024,
|
||||
unit_scale=True,
|
||||
ascii=True,
|
||||
smoothing=0
|
||||
) as pbar:
|
||||
with open(download_path, "wb") as of:
|
||||
resp = RQ.get(
|
||||
|
@ -173,7 +193,7 @@ def download(url, folder):
|
|||
"-s",
|
||||
default=systems_path,
|
||||
metavar="<path>",
|
||||
help="Path to stars.csv",
|
||||
help="Path to systemsWithCoordinates.json",
|
||||
type=click.Path(exists=True, dir_okay=False),
|
||||
show_default=True,
|
||||
)
|
||||
|
@ -196,7 +216,7 @@ def download(url, folder):
|
|||
show_default=True,
|
||||
)
|
||||
def preprocess(systems, bodies, output):
|
||||
"Preprocess EDSM dumps"
|
||||
"Preprocess EDSM dumps."
|
||||
with click.progressbar(
|
||||
length=100, label="", show_percent=True, item_show_func=lambda v: v, width=50
|
||||
) as pbar:
|
||||
|
@ -289,12 +309,20 @@ def preprocess(systems, bodies, output):
|
|||
"-m",
|
||||
default=cfg["route.mode"],
|
||||
help="Search mode",
|
||||
type=click.Choice(["bfs", "a-star", "greedy"]),
|
||||
type=click.Choice(["bfs","bfs_old", "a-star", "greedy"]),
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--workers",
|
||||
"-w",
|
||||
metavar="<int>",
|
||||
default=1,
|
||||
help="Number of worker threads (more is not always better)",
|
||||
show_default=True,
|
||||
)
|
||||
@click.argument("systems", nargs=-1)
|
||||
def route(**kwargs):
|
||||
"Compute a route"
|
||||
"Compute a route."
|
||||
if len(kwargs["systems"]) < 2:
|
||||
exit("Need at least two systems to plot a route")
|
||||
if kwargs["prune"] == (0, 0):
|
||||
|
@ -302,7 +330,7 @@ def route(**kwargs):
|
|||
|
||||
def to_string(state):
|
||||
if state:
|
||||
return "{prc_done:.2f}% [N:{depth} Q:{queue_size} D:{d_rem:.2f} Ly] {system}".format(
|
||||
return "{prc_done:.2f}% [N:{depth} | Q:{queue_size} | D:{d_rem:.2f} Ly | S: {n_seen} ({prc_seen:.2f}%)] {system}".format(
|
||||
**state
|
||||
)
|
||||
|
||||
|
@ -330,6 +358,7 @@ def route(**kwargs):
|
|||
kwargs["factor"],
|
||||
None,
|
||||
kwargs["path"],
|
||||
kwargs["workers"]
|
||||
]
|
||||
with click.progressbar(
|
||||
length=100,
|
||||
|
@ -359,7 +388,7 @@ def route(**kwargs):
|
|||
pbar.update(0)
|
||||
for n, jump in enumerate(state.get("return", []), 1):
|
||||
jump["n"] = n
|
||||
if jump["body"].index(jump["system"]) == -1:
|
||||
if jump["body"].find(jump["system"]) == -1:
|
||||
jump["where"] = "[{body}] in [{system}]".format(**jump)
|
||||
else:
|
||||
jump["where"] = "[{body}]".format(**jump)
|
||||
|
@ -397,6 +426,7 @@ def route(**kwargs):
|
|||
def precompute(*args, **kwargs):
|
||||
"Precompute routing graph"
|
||||
print("PreComp:", args, kwargs)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -55,5 +55,8 @@ cfg.init("folders.data_dir", os.path.join(config_dir, "data"), comment="Data dir
|
|||
|
||||
cfg.init("GUI.theme", "dark", comment="GUI theme to use")
|
||||
|
||||
cfg.init("web.port",3777,comment="Port to bind to")
|
||||
cfg.init("web.host","0.0.0.0",comment="Address to bind to")
|
||||
cfg.init("web.debug",False,comment="Run using debug server")
|
||||
|
||||
cfg.sync()
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Form implementation generated from reading ui file 'D:\devel\rust\ed_lrr_gui\ed_lrr_gui\gui\ed_lrr.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.13.1
|
||||
# Created by: PyQt5 UI code generator 5.14.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
|
@ -15,9 +15,7 @@ class Ui_ED_LRR(object):
|
|||
ED_LRR.setObjectName("ED_LRR")
|
||||
ED_LRR.setEnabled(True)
|
||||
ED_LRR.resize(577, 500)
|
||||
sizePolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed
|
||||
)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(ED_LRR.sizePolicy().hasHeightForWidth())
|
||||
|
@ -28,14 +26,10 @@ class Ui_ED_LRR(object):
|
|||
ED_LRR.setDocumentMode(False)
|
||||
ED_LRR.setTabShape(QtWidgets.QTabWidget.Rounded)
|
||||
self.centralwidget = QtWidgets.QWidget(ED_LRR)
|
||||
sizePolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred
|
||||
)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(
|
||||
self.centralwidget.sizePolicy().hasHeightForWidth()
|
||||
)
|
||||
sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth())
|
||||
self.centralwidget.setSizePolicy(sizePolicy)
|
||||
self.centralwidget.setObjectName("centralwidget")
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
|
||||
|
@ -55,39 +49,27 @@ class Ui_ED_LRR(object):
|
|||
self.formLayout.setObjectName("formLayout")
|
||||
self.lbl_bodies_dl = QtWidgets.QLabel(self.tab_download)
|
||||
self.lbl_bodies_dl.setObjectName("lbl_bodies_dl")
|
||||
self.formLayout.setWidget(
|
||||
1, QtWidgets.QFormLayout.LabelRole, self.lbl_bodies_dl
|
||||
)
|
||||
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.lbl_bodies_dl)
|
||||
self.lbl_systems_dl = QtWidgets.QLabel(self.tab_download)
|
||||
self.lbl_systems_dl.setObjectName("lbl_systems_dl")
|
||||
self.formLayout.setWidget(
|
||||
3, QtWidgets.QFormLayout.LabelRole, self.lbl_systems_dl
|
||||
)
|
||||
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.lbl_systems_dl)
|
||||
self.inp_bodies_dl = QtWidgets.QComboBox(self.tab_download)
|
||||
self.inp_bodies_dl.setEditable(True)
|
||||
self.inp_bodies_dl.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
|
||||
self.inp_bodies_dl.setObjectName("inp_bodies_dl")
|
||||
self.formLayout.setWidget(
|
||||
1, QtWidgets.QFormLayout.FieldRole, self.inp_bodies_dl
|
||||
)
|
||||
self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.inp_bodies_dl)
|
||||
self.inp_systems_dl = QtWidgets.QComboBox(self.tab_download)
|
||||
self.inp_systems_dl.setEditable(True)
|
||||
self.inp_systems_dl.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
|
||||
self.inp_systems_dl.setObjectName("inp_systems_dl")
|
||||
self.formLayout.setWidget(
|
||||
3, QtWidgets.QFormLayout.FieldRole, self.inp_systems_dl
|
||||
)
|
||||
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.inp_systems_dl)
|
||||
self.gridLayout = QtWidgets.QGridLayout()
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.inp_bodies_dest_dl = QtWidgets.QComboBox(self.tab_download)
|
||||
sizePolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
|
||||
)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(
|
||||
self.inp_bodies_dest_dl.sizePolicy().hasHeightForWidth()
|
||||
)
|
||||
sizePolicy.setHeightForWidth(self.inp_bodies_dest_dl.sizePolicy().hasHeightForWidth())
|
||||
self.inp_bodies_dest_dl.setSizePolicy(sizePolicy)
|
||||
self.inp_bodies_dest_dl.setEditable(False)
|
||||
self.inp_bodies_dest_dl.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
|
||||
|
@ -103,14 +85,10 @@ class Ui_ED_LRR(object):
|
|||
self.btn_systems_dest_browse_dl.setObjectName("btn_systems_dest_browse_dl")
|
||||
self.gridLayout_2.addWidget(self.btn_systems_dest_browse_dl, 0, 1, 1, 1)
|
||||
self.inp_systems_dest_dl = QtWidgets.QComboBox(self.tab_download)
|
||||
sizePolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
|
||||
)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(
|
||||
self.inp_systems_dest_dl.sizePolicy().hasHeightForWidth()
|
||||
)
|
||||
sizePolicy.setHeightForWidth(self.inp_systems_dest_dl.sizePolicy().hasHeightForWidth())
|
||||
self.inp_systems_dest_dl.setSizePolicy(sizePolicy)
|
||||
self.inp_systems_dest_dl.setEditable(False)
|
||||
self.inp_systems_dest_dl.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
|
||||
|
@ -133,79 +111,57 @@ class Ui_ED_LRR(object):
|
|||
self.formLayout_3.setObjectName("formLayout_3")
|
||||
self.lbl_bodies_pp = QtWidgets.QLabel(self.tab_preprocess)
|
||||
self.lbl_bodies_pp.setObjectName("lbl_bodies_pp")
|
||||
self.formLayout_3.setWidget(
|
||||
0, QtWidgets.QFormLayout.LabelRole, self.lbl_bodies_pp
|
||||
)
|
||||
self.formLayout_3.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.lbl_bodies_pp)
|
||||
self.gr_bodies_pp = QtWidgets.QGridLayout()
|
||||
self.gr_bodies_pp.setObjectName("gr_bodies_pp")
|
||||
self.btn_bodies_browse_pp = QtWidgets.QPushButton(self.tab_preprocess)
|
||||
self.btn_bodies_browse_pp.setObjectName("btn_bodies_browse_pp")
|
||||
self.gr_bodies_pp.addWidget(self.btn_bodies_browse_pp, 0, 1, 1, 1)
|
||||
self.inp_bodies_pp = QtWidgets.QComboBox(self.tab_preprocess)
|
||||
sizePolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
|
||||
)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(
|
||||
self.inp_bodies_pp.sizePolicy().hasHeightForWidth()
|
||||
)
|
||||
sizePolicy.setHeightForWidth(self.inp_bodies_pp.sizePolicy().hasHeightForWidth())
|
||||
self.inp_bodies_pp.setSizePolicy(sizePolicy)
|
||||
self.inp_bodies_pp.setEditable(False)
|
||||
self.inp_bodies_pp.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
|
||||
self.inp_bodies_pp.setObjectName("inp_bodies_pp")
|
||||
self.gr_bodies_pp.addWidget(self.inp_bodies_pp, 0, 0, 1, 1)
|
||||
self.formLayout_3.setLayout(
|
||||
0, QtWidgets.QFormLayout.FieldRole, self.gr_bodies_pp
|
||||
)
|
||||
self.formLayout_3.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.gr_bodies_pp)
|
||||
self.lbl_systems_pp = QtWidgets.QLabel(self.tab_preprocess)
|
||||
self.lbl_systems_pp.setObjectName("lbl_systems_pp")
|
||||
self.formLayout_3.setWidget(
|
||||
1, QtWidgets.QFormLayout.LabelRole, self.lbl_systems_pp
|
||||
)
|
||||
self.formLayout_3.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.lbl_systems_pp)
|
||||
self.gr_systems_pp = QtWidgets.QGridLayout()
|
||||
self.gr_systems_pp.setObjectName("gr_systems_pp")
|
||||
self.btn_systems_browse_pp = QtWidgets.QPushButton(self.tab_preprocess)
|
||||
self.btn_systems_browse_pp.setObjectName("btn_systems_browse_pp")
|
||||
self.gr_systems_pp.addWidget(self.btn_systems_browse_pp, 0, 1, 1, 1)
|
||||
self.inp_systems_pp = QtWidgets.QComboBox(self.tab_preprocess)
|
||||
sizePolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
|
||||
)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(
|
||||
self.inp_systems_pp.sizePolicy().hasHeightForWidth()
|
||||
)
|
||||
sizePolicy.setHeightForWidth(self.inp_systems_pp.sizePolicy().hasHeightForWidth())
|
||||
self.inp_systems_pp.setSizePolicy(sizePolicy)
|
||||
self.inp_systems_pp.setEditable(False)
|
||||
self.inp_systems_pp.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
|
||||
self.inp_systems_pp.setObjectName("inp_systems_pp")
|
||||
self.gr_systems_pp.addWidget(self.inp_systems_pp, 0, 0, 1, 1)
|
||||
self.formLayout_3.setLayout(
|
||||
1, QtWidgets.QFormLayout.FieldRole, self.gr_systems_pp
|
||||
)
|
||||
self.formLayout_3.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.gr_systems_pp)
|
||||
self.lbl_out_pp = QtWidgets.QLabel(self.tab_preprocess)
|
||||
self.lbl_out_pp.setObjectName("lbl_out_pp")
|
||||
self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.lbl_out_pp)
|
||||
self.gr_out_grid_pp = QtWidgets.QGridLayout()
|
||||
self.gr_out_grid_pp.setObjectName("gr_out_grid_pp")
|
||||
self.btn_out_browse_pp = QtWidgets.QPushButton(self.tab_preprocess)
|
||||
sizePolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed
|
||||
)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(
|
||||
self.btn_out_browse_pp.sizePolicy().hasHeightForWidth()
|
||||
)
|
||||
sizePolicy.setHeightForWidth(self.btn_out_browse_pp.sizePolicy().hasHeightForWidth())
|
||||
self.btn_out_browse_pp.setSizePolicy(sizePolicy)
|
||||
self.btn_out_browse_pp.setObjectName("btn_out_browse_pp")
|
||||
self.gr_out_grid_pp.addWidget(self.btn_out_browse_pp, 0, 1, 1, 1)
|
||||
self.inp_out_pp = QtWidgets.QComboBox(self.tab_preprocess)
|
||||
sizePolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
|
||||
)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.inp_out_pp.sizePolicy().hasHeightForWidth())
|
||||
|
@ -214,14 +170,10 @@ class Ui_ED_LRR(object):
|
|||
self.inp_out_pp.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
|
||||
self.inp_out_pp.setObjectName("inp_out_pp")
|
||||
self.gr_out_grid_pp.addWidget(self.inp_out_pp, 0, 0, 1, 1)
|
||||
self.formLayout_3.setLayout(
|
||||
2, QtWidgets.QFormLayout.FieldRole, self.gr_out_grid_pp
|
||||
)
|
||||
self.formLayout_3.setLayout(2, QtWidgets.QFormLayout.FieldRole, self.gr_out_grid_pp)
|
||||
self.btn_preprocess = QtWidgets.QPushButton(self.tab_preprocess)
|
||||
self.btn_preprocess.setObjectName("btn_preprocess")
|
||||
self.formLayout_3.setWidget(
|
||||
3, QtWidgets.QFormLayout.LabelRole, self.btn_preprocess
|
||||
)
|
||||
self.formLayout_3.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.btn_preprocess)
|
||||
self.tabs.addTab(self.tab_preprocess, "")
|
||||
self.tab_route = QtWidgets.QWidget()
|
||||
self.tab_route.setObjectName("tab_route")
|
||||
|
@ -229,18 +181,14 @@ class Ui_ED_LRR(object):
|
|||
self.formLayout_2.setObjectName("formLayout_2")
|
||||
self.lbl_sys_lst = QtWidgets.QLabel(self.tab_route)
|
||||
self.lbl_sys_lst.setObjectName("lbl_sys_lst")
|
||||
self.formLayout_2.setWidget(
|
||||
0, QtWidgets.QFormLayout.LabelRole, self.lbl_sys_lst
|
||||
)
|
||||
self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.lbl_sys_lst)
|
||||
self.gr_sys = QtWidgets.QGridLayout()
|
||||
self.gr_sys.setObjectName("gr_sys")
|
||||
self.btn_sys_lst_browse = QtWidgets.QPushButton(self.tab_route)
|
||||
self.btn_sys_lst_browse.setObjectName("btn_sys_lst_browse")
|
||||
self.gr_sys.addWidget(self.btn_sys_lst_browse, 0, 1, 1, 1)
|
||||
self.inp_sys_lst = QtWidgets.QComboBox(self.tab_route)
|
||||
sizePolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
|
||||
)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.inp_sys_lst.sizePolicy().hasHeightForWidth())
|
||||
|
@ -273,44 +221,32 @@ class Ui_ED_LRR(object):
|
|||
self.formLayout_2.setLayout(3, QtWidgets.QFormLayout.FieldRole, self.gr_mode)
|
||||
self.chk_permute = QtWidgets.QCheckBox(self.tab_route)
|
||||
self.chk_permute.setObjectName("chk_permute")
|
||||
self.formLayout_2.setWidget(
|
||||
4, QtWidgets.QFormLayout.LabelRole, self.chk_permute
|
||||
)
|
||||
self.formLayout_2.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.chk_permute)
|
||||
self.gridLayout_4 = QtWidgets.QGridLayout()
|
||||
self.gridLayout_4.setObjectName("gridLayout_4")
|
||||
self.chk_permute_keep_last = QtWidgets.QCheckBox(self.tab_route)
|
||||
self.chk_permute_keep_last.setObjectName("chk_permute_keep_last")
|
||||
self.gridLayout_4.addWidget(self.chk_permute_keep_last, 0, 3, 1, 1)
|
||||
self.chk_permute_keep_first = QtWidgets.QCheckBox(self.tab_route)
|
||||
sizePolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed
|
||||
)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(
|
||||
self.chk_permute_keep_first.sizePolicy().hasHeightForWidth()
|
||||
)
|
||||
sizePolicy.setHeightForWidth(self.chk_permute_keep_first.sizePolicy().hasHeightForWidth())
|
||||
self.chk_permute_keep_first.setSizePolicy(sizePolicy)
|
||||
self.chk_permute_keep_first.setTristate(False)
|
||||
self.chk_permute_keep_first.setObjectName("chk_permute_keep_first")
|
||||
self.gridLayout_4.addWidget(self.chk_permute_keep_first, 0, 2, 1, 1)
|
||||
self.lbl_keep = QtWidgets.QLabel(self.tab_route)
|
||||
sizePolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred
|
||||
)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.lbl_keep.sizePolicy().hasHeightForWidth())
|
||||
self.lbl_keep.setSizePolicy(sizePolicy)
|
||||
self.lbl_keep.setObjectName("lbl_keep")
|
||||
self.gridLayout_4.addWidget(self.lbl_keep, 0, 1, 1, 1)
|
||||
self.formLayout_2.setLayout(
|
||||
4, QtWidgets.QFormLayout.FieldRole, self.gridLayout_4
|
||||
)
|
||||
self.formLayout_2.setLayout(4, QtWidgets.QFormLayout.FieldRole, self.gridLayout_4)
|
||||
self.lst_sys = QtWidgets.QTreeWidget(self.tab_route)
|
||||
sizePolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
|
||||
)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.lst_sys.sizePolicy().hasHeightForWidth())
|
||||
|
@ -417,27 +353,17 @@ class Ui_ED_LRR(object):
|
|||
|
||||
def retranslateUi(self, ED_LRR):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
ED_LRR.setWindowTitle(
|
||||
_translate("ED_LRR", "Elite: Dangerous Long Range Route Plotter")
|
||||
)
|
||||
ED_LRR.setWindowTitle(_translate("ED_LRR", "Elite: Dangerous Long Range Route Plotter"))
|
||||
self.lbl_bodies_dl.setText(_translate("ED_LRR", "bodies.json"))
|
||||
self.lbl_systems_dl.setText(_translate("ED_LRR", "systemsWithCoordinates.json"))
|
||||
self.inp_bodies_dl.setCurrentText(
|
||||
_translate("ED_LRR", "https://www.edsm.net/dump/bodies.json")
|
||||
)
|
||||
self.inp_systems_dl.setCurrentText(
|
||||
_translate(
|
||||
"ED_LRR", "https://www.edsm.net/dump/systemsWithCoordinates.json"
|
||||
)
|
||||
)
|
||||
self.inp_bodies_dl.setCurrentText(_translate("ED_LRR", "https://www.edsm.net/dump/bodies.json"))
|
||||
self.inp_systems_dl.setCurrentText(_translate("ED_LRR", "https://www.edsm.net/dump/systemsWithCoordinates.json"))
|
||||
self.btn_bodies_dest_browse_dl.setText(_translate("ED_LRR", "..."))
|
||||
self.btn_systems_dest_browse_dl.setText(_translate("ED_LRR", "..."))
|
||||
self.btn_download.setText(_translate("ED_LRR", "Download"))
|
||||
self.label.setText(_translate("ED_LRR", "Download path"))
|
||||
self.label_2.setText(_translate("ED_LRR", "Download path"))
|
||||
self.tabs.setTabText(
|
||||
self.tabs.indexOf(self.tab_download), _translate("ED_LRR", "Download")
|
||||
)
|
||||
self.tabs.setTabText(self.tabs.indexOf(self.tab_download), _translate("ED_LRR", "Download"))
|
||||
self.lbl_bodies_pp.setText(_translate("ED_LRR", "bodies.json"))
|
||||
self.btn_bodies_browse_pp.setText(_translate("ED_LRR", "..."))
|
||||
self.lbl_systems_pp.setText(_translate("ED_LRR", "systemsWithCoordinates.json"))
|
||||
|
@ -445,9 +371,7 @@ class Ui_ED_LRR(object):
|
|||
self.lbl_out_pp.setText(_translate("ED_LRR", "Output"))
|
||||
self.btn_out_browse_pp.setText(_translate("ED_LRR", "..."))
|
||||
self.btn_preprocess.setText(_translate("ED_LRR", "Preprocess"))
|
||||
self.tabs.setTabText(
|
||||
self.tabs.indexOf(self.tab_preprocess), _translate("ED_LRR", "Preprocess")
|
||||
)
|
||||
self.tabs.setTabText(self.tabs.indexOf(self.tab_preprocess), _translate("ED_LRR", "Preprocess"))
|
||||
self.lbl_sys_lst.setText(_translate("ED_LRR", "System List"))
|
||||
self.btn_sys_lst_browse.setText(_translate("ED_LRR", "..."))
|
||||
self.btn_add.setText(_translate("ED_LRR", "Add"))
|
||||
|
@ -469,12 +393,8 @@ class Ui_ED_LRR(object):
|
|||
self.chk_primary.setText(_translate("ED_LRR", "Primary Stars Only"))
|
||||
self.lbl_mode.setText(_translate("ED_LRR", "Mode"))
|
||||
self.btn_go.setText(_translate("ED_LRR", "GO!"))
|
||||
self.tabs.setTabText(
|
||||
self.tabs.indexOf(self.tab_route), _translate("ED_LRR", "Route")
|
||||
)
|
||||
self.tabs.setTabText(
|
||||
self.tabs.indexOf(self.tab_log), _translate("ED_LRR", "Log")
|
||||
)
|
||||
self.tabs.setTabText(self.tabs.indexOf(self.tab_route), _translate("ED_LRR", "Route"))
|
||||
self.tabs.setTabText(self.tabs.indexOf(self.tab_log), _translate("ED_LRR", "Log"))
|
||||
self.menu_file.setTitle(_translate("ED_LRR", "File"))
|
||||
self.menuWindow.setTitle(_translate("ED_LRR", "Window"))
|
||||
self.menuStyle.setTitle(_translate("ED_LRR", "Style"))
|
||||
|
|
|
@ -8,7 +8,6 @@ import sys
|
|||
from sys import exit
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
import _ed_lrr
|
||||
import ed_lrr_gui
|
||||
import requests as RQ
|
||||
|
@ -436,7 +435,7 @@ class ED_LRR(Ui_ED_LRR):
|
|||
return
|
||||
self.bar_status.clearMessage()
|
||||
print(self.systems)
|
||||
systems = [str(s["id"]) for s in self.systems]
|
||||
systems = [s["id"] for s in self.systems]
|
||||
jump_range = self.sb_range.value()
|
||||
if jump_range == 0:
|
||||
self.error(
|
||||
|
@ -466,13 +465,16 @@ class ED_LRR(Ui_ED_LRR):
|
|||
print(
|
||||
systems,
|
||||
jump_range,
|
||||
None,
|
||||
mode,
|
||||
primary,
|
||||
permute,
|
||||
(keep_first, keep_last),
|
||||
keep_first,
|
||||
keep_last,
|
||||
greedyness,
|
||||
path,
|
||||
precomp,
|
||||
path,
|
||||
os.cpu_count()-1
|
||||
)
|
||||
if not self.current_job:
|
||||
self.bar_status.showMessage("Computing Route...")
|
||||
|
@ -490,6 +492,7 @@ class ED_LRR(Ui_ED_LRR):
|
|||
greedyness,
|
||||
precomp,
|
||||
path,
|
||||
os.cpu_count()-1
|
||||
)
|
||||
else:
|
||||
self.error("there is already a job running!")
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Form implementation generated from reading ui file 'D:\devel\rust\ed_lrr_gui\ed_lrr_gui\gui\widget_route.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.13.1
|
||||
# Created by: PyQt5 UI code generator 5.14.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
|
@ -25,9 +25,7 @@ class Ui_diag_route(object):
|
|||
self.lst_route.setAlternatingRowColors(True)
|
||||
self.lst_route.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
|
||||
self.lst_route.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerItem)
|
||||
self.lst_route.setHorizontalScrollMode(
|
||||
QtWidgets.QAbstractItemView.ScrollPerPixel
|
||||
)
|
||||
self.lst_route.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
|
||||
self.lst_route.setItemsExpandable(False)
|
||||
self.lst_route.setAllColumnsShowFocus(False)
|
||||
self.lst_route.setObjectName("lst_route")
|
||||
|
@ -55,11 +53,7 @@ class Ui_diag_route(object):
|
|||
self.lst_route.headerItem().setText(0, _translate("diag_route", "Num"))
|
||||
self.lst_route.headerItem().setText(1, _translate("diag_route", "System"))
|
||||
self.lst_route.headerItem().setText(2, _translate("diag_route", "Body"))
|
||||
self.lst_route.headerItem().setText(
|
||||
3, _translate("diag_route", "Distance (Ls)")
|
||||
)
|
||||
self.chk_copy.setText(
|
||||
_translate("diag_route", "Auto-copy next hop to clipboard")
|
||||
)
|
||||
self.lst_route.headerItem().setText(3, _translate("diag_route", "Distance (Ls)"))
|
||||
self.chk_copy.setText(_translate("diag_route", "Auto-copy next hop to clipboard"))
|
||||
self.btn_close.setText(_translate("diag_route", "Close"))
|
||||
self.btn_export.setText(_translate("diag_route", "Export"))
|
||||
|
|
|
@ -10,7 +10,6 @@ def dist(p1, p2):
|
|||
s += (c1 - c2) ** 2
|
||||
return s ** 0.5
|
||||
|
||||
|
||||
colors = {
|
||||
"O": "#0000FF",
|
||||
"B": "#140AF0",
|
||||
|
|
|
@ -20,6 +20,7 @@ class Router(Process):
|
|||
self.queue.put({"status": state})
|
||||
|
||||
def run(self):
|
||||
print("Route(): ",self.args,self.kwargs)
|
||||
route = _ed_lrr.route(*self.args, **self.kwargs)
|
||||
self.queue.put({"return": route})
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
from .app import app, templates, db
|
|
@ -1,100 +0,0 @@
|
|||
from flask import Flask, jsonify
|
||||
import uuid
|
||||
import json
|
||||
from webargs import fields, validate
|
||||
from webargs.flaskparser import use_args
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy_utils import Timestamp, generic_repr
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref
|
||||
from sqlalchemy.types import Float, String, Boolean
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///jobs.db"
|
||||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
|
||||
@generic_repr
|
||||
class Job(db.Model, Timestamp):
|
||||
id = db.Column(db.String, default=lambda: str(uuid.uuid4()), primary_key=True)
|
||||
jump_range = db.Column(db.Float, nullable=False)
|
||||
mode = db.Column(db.String, default="bfs")
|
||||
systems = db.Column(db.String)
|
||||
permute = db.Column(db.String, default=None, nullable=True)
|
||||
primary = db.Column(db.Boolean, default=False)
|
||||
factor = db.Column(db.Float, default=0.5)
|
||||
done = db.Column(db.DateTime, nullable=True, default=None)
|
||||
started = db.Column(db.DateTime, nullable=True, default=None)
|
||||
progress = db.Column(db.Float, default=0.0)
|
||||
|
||||
# ============================================================
|
||||
|
||||
@classmethod
|
||||
def new(cls, **kwargs):
|
||||
obj = cls(**kwargs)
|
||||
db.session.add(obj)
|
||||
db.session.commit()
|
||||
print(obj)
|
||||
return obj
|
||||
|
||||
@property
|
||||
def dict(self):
|
||||
ret = {}
|
||||
for col in self.__table__.columns:
|
||||
ret[col.name] = getattr(self, col.name)
|
||||
ret["systems"] = json.loads(ret["systems"])
|
||||
return ret
|
||||
|
||||
@dict.setter
|
||||
def set_dict(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
db.create_all()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@app.errorhandler(422)
|
||||
@app.errorhandler(400)
|
||||
def handle_error(err):
|
||||
headers = err.data.get("headers", None)
|
||||
messages = err.data.get("messages", ["Invalid request."])
|
||||
if headers:
|
||||
return jsonify({"errors": messages}), err.code, headers
|
||||
else:
|
||||
return jsonify({"errors": messages}), err.code
|
||||
|
||||
|
||||
@app.route("/route", methods=["GET", "POST"])
|
||||
@use_args(
|
||||
{
|
||||
"jump_range": fields.Float(required=True),
|
||||
"mode": fields.String(
|
||||
missing="bfs", validate=validate.OneOf(["bfs", "greedy", "a-star"])
|
||||
),
|
||||
"systems": fields.DelimitedList(fields.String, required=True),
|
||||
"permute": fields.String(
|
||||
missing=None,
|
||||
validate=validate.OneOf(["all", "keep_first", "keep_last", "keep_both"]),
|
||||
),
|
||||
"primary": fields.Boolean(missing=False),
|
||||
"factor": fields.Float(missing=0.5),
|
||||
}
|
||||
)
|
||||
def route(args):
|
||||
args["systems"] = json.dumps(args["systems"])
|
||||
for k, v in args.items():
|
||||
print(k, v)
|
||||
return jsonify({"id": Job.new(**args).id})
|
||||
|
||||
|
||||
@app.route("/status/<uuid:job_id>")
|
||||
def status(job_id):
|
||||
job = db.session.query(Job).get_or_404(str(job_id))
|
||||
return jsonify(job.dict)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=3777, debug=True)
|
665
ed_lrr_gui/web/app.py
Normal file
665
ed_lrr_gui/web/app.py
Normal file
|
@ -0,0 +1,665 @@
|
|||
from flask import (
|
||||
Flask,
|
||||
jsonify,
|
||||
session,
|
||||
render_template,
|
||||
redirect,
|
||||
url_for,
|
||||
send_from_directory,
|
||||
request,
|
||||
flash,
|
||||
current_app
|
||||
)
|
||||
from flask.json.tag import JSONTag
|
||||
import uuid
|
||||
import pickle
|
||||
import os
|
||||
import time
|
||||
import random
|
||||
import base64
|
||||
import gevent
|
||||
from functools import wraps
|
||||
from concurrent.futures.process import BrokenProcessPool
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from multiprocessing import Queue
|
||||
from webargs import fields, validate
|
||||
from webargs.flaskparser import use_kwargs
|
||||
|
||||
from flask_executor import Executor
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_bootstrap import Bootstrap
|
||||
|
||||
from flask_nav import Nav, register_renderer
|
||||
from flask_nav.elements import Navbar, View
|
||||
|
||||
from flask_admin import Admin
|
||||
from flask_admin.contrib.sqla import ModelView
|
||||
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
|
||||
from flask_login import (
|
||||
LoginManager,
|
||||
current_user,
|
||||
logout_user,
|
||||
UserMixin,
|
||||
AnonymousUserMixin,
|
||||
login_user,
|
||||
login_required,
|
||||
)
|
||||
|
||||
from flask_debugtoolbar import DebugToolbarExtension
|
||||
|
||||
from werkzeug.http import HTTP_STATUS_CODES
|
||||
from sqlalchemy_utils import generic_repr, JSONType, PasswordType, UUIDType
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref
|
||||
from sqlalchemy.types import Float, String, DateTime
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
from .forms import RouteForm, LoginForm, RegisterForm, ChangePasswordForm
|
||||
from .utils import prepare_route, BootsrapRenderer, is_safe_url
|
||||
|
||||
import _ed_lrr as ed_lrr
|
||||
|
||||
templates = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
|
||||
app = Flask(__name__, template_folder=templates)
|
||||
app.config.from_pyfile("config.py")
|
||||
|
||||
executor = Executor(app)
|
||||
db = SQLAlchemy(app)
|
||||
bootstrap = Bootstrap(app)
|
||||
csrf = CSRFProtect(app)
|
||||
nav = Nav(app)
|
||||
login_manager = LoginManager(app)
|
||||
login_manager.login_view = "login"
|
||||
login_manager.session_protection = "strong"
|
||||
admin = Admin(app, name="ED_LRR", template_mode="bootstrap3")
|
||||
app.debug=True
|
||||
toolbar = DebugToolbarExtension(app)
|
||||
|
||||
|
||||
def wants_json_response():
|
||||
return request.accept_mimetypes['application/json'] >= \
|
||||
request.accept_mimetypes['text/html']
|
||||
|
||||
|
||||
@app.errorhandler(422)
|
||||
@app.errorhandler(400)
|
||||
@app.errorhandler(500)
|
||||
@app.errorhandler(404)
|
||||
def handle_error(err):
|
||||
if wants_json_response():
|
||||
return jsonify(error=str(err),code=err.code), err.code
|
||||
templates=["error/{}.html".format(err.code),"error/default.html"]
|
||||
try:
|
||||
print(dir(err))
|
||||
return render_template(templates,error=err),err.code
|
||||
except TemplateNotFound:
|
||||
return err.get_response()
|
||||
|
||||
def role_required(*roles):
|
||||
def wrapper(fn):
|
||||
@wraps(fn)
|
||||
def decorated_view(*args, **kwargs):
|
||||
if not current_user.is_authenticated():
|
||||
return current_app.login_manager.unauthorized()
|
||||
has_role=False
|
||||
user=current_app.login_manager.reload_user()
|
||||
for role in roles:
|
||||
has_role|=user.has_role(role)
|
||||
if not has_role:
|
||||
return current_app.login_manager.unauthorized()
|
||||
return fn(*args, **kwargs)
|
||||
return decorated_view
|
||||
return wrapper
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_name):
|
||||
return User.query.get(user_name)
|
||||
|
||||
|
||||
@login_manager.request_loader
|
||||
def load_user_from_header(header_val):
|
||||
for api_key in [request.args.get('api_key'),request.headers.get('X-API-Key')]:
|
||||
if api_key:
|
||||
user = User.query.filter_by(api_key=api_key).one_or_none()
|
||||
if user:
|
||||
return user
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def left_nav():
|
||||
links=[View("Home", "index"),View("Route", "route"),View("Jobs", "status",job_id=None)]
|
||||
if current_user.has_role('admin') or current_user.has_role('worker_host'):
|
||||
links.insert(2,View("Workers","worker"))
|
||||
return Navbar(
|
||||
"E:D LRR",
|
||||
*links
|
||||
)
|
||||
|
||||
|
||||
def right_nav():
|
||||
links = [View("Login", "login"), View("Register", "register")]
|
||||
if current_user.is_authenticated:
|
||||
links = [View("Change Password", "change_password"), View("Logout", "logout")]
|
||||
if current_user.has_role('admin'):
|
||||
links = [View("Admin", "admin.index")] + links
|
||||
return Navbar("", *links)
|
||||
|
||||
|
||||
register_renderer(app, "bootstrap4", BootsrapRenderer)
|
||||
nav.register_element("left_nav", left_nav)
|
||||
nav.register_element("right_nav", right_nav)
|
||||
|
||||
|
||||
def compute_route(args, kwargs):
|
||||
return ed_lrr.route(*args, **kwargs)
|
||||
|
||||
|
||||
class AnonymousUser(AnonymousUserMixin):
|
||||
|
||||
def has_role(self,role):
|
||||
return False
|
||||
|
||||
@property
|
||||
def roles(self):
|
||||
return []
|
||||
|
||||
@roles.setter
|
||||
def __set_roles(self,value):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
login_manager.anonymous_user = AnonymousUser
|
||||
|
||||
|
||||
@generic_repr
|
||||
class Worker(db.Model):
|
||||
id = db.Column(
|
||||
UUIDType(binary=False, native=False), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
name = db.Column(db.String, unique=True)
|
||||
current_job=db.Column(UUIDType(binary=False, native=False),db.ForeignKey("job.id"), nullable=True,default=None)
|
||||
job = relationship('Job',backref="workers")
|
||||
last_active = db.Column(DateTime, nullable=True,default=None)
|
||||
owner_name = db.Column(
|
||||
db.String, db.ForeignKey("user.name"), nullable=True,index=True
|
||||
)
|
||||
owner = relationship("User",backref="workers")
|
||||
|
||||
user_roles = db.Table('user_roles',
|
||||
db.Column('user_name', db.String, db.ForeignKey('user.name'),primary_key=True),
|
||||
db.Column('role_name', db.String, db.ForeignKey('role.name'),primary_key=True)
|
||||
)
|
||||
|
||||
class Role(db.Model):
|
||||
name = db.Column(db.String, unique=True,index=True,primary_key=True)
|
||||
|
||||
def __init__(self,name):
|
||||
self.name=name
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
name = db.Column(db.String, unique=True,index=True,primary_key=True)
|
||||
is_active = db.Column(db.Boolean, default=False)
|
||||
api_key = db.Column(
|
||||
UUIDType(binary=False, native=False), nullable=True, default=uuid.uuid4,index=True
|
||||
)
|
||||
password = db.Column(PasswordType(schemes=["pbkdf2_sha512"], max_length=256))
|
||||
created = db.Column(DateTime, default=datetime.today)
|
||||
|
||||
roles = db.relationship("Role",secondary="user_roles")
|
||||
|
||||
def add_roles(self,roles):
|
||||
for role_name in roles:
|
||||
role=Role.query.filter_by(name=role_name).one()
|
||||
if not role in self.roles:
|
||||
self.roles.append(role)
|
||||
db.session.commit()
|
||||
|
||||
def has_role(self,role_name):
|
||||
return Role.query.join(User.roles).filter(User.name==self.name,Role.name==role_name).count()>0
|
||||
return ret
|
||||
|
||||
def reset_api_key(self):
|
||||
self.api_key=uuid,uuid4()
|
||||
db.session.add(self)
|
||||
db.session.comiit()
|
||||
|
||||
def get_id(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Job(db.Model):
|
||||
id = db.Column(
|
||||
UUIDType(binary=False, native=False), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
user_name = db.Column(
|
||||
db.String, db.ForeignKey("user.name"), nullable=True,index=True
|
||||
)
|
||||
func = db.Column(db.String)
|
||||
args = db.Column(JSONType)
|
||||
kwargs = db.Column(JSONType)
|
||||
state = db.Column(JSONType, default={})
|
||||
priority = db.Column(db.Integer, default=0,nullable=True)
|
||||
created = db.Column(DateTime, default=datetime.today)
|
||||
finished = db.Column(DateTime, nullable=True, default=None)
|
||||
started = db.Column(DateTime, nullable=True, default=None)
|
||||
last_update = db.Column(DateTime, nullable=True, default=None)
|
||||
user = relationship("User",backref="jobs")
|
||||
# ============================================================
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.id)
|
||||
|
||||
|
||||
@property
|
||||
def future(self):
|
||||
fut = executor.futures._futures.get(self.id)
|
||||
return fut
|
||||
|
||||
@property
|
||||
def sort_key(self):
|
||||
state_priorities={"Queued":0,"Starting":1,"Error":1,"Stalled":1,"Running":1}
|
||||
status_key=state_priorities.get(self.status[1],-1)+1
|
||||
user=1-int(self.user is not None)
|
||||
return (user,-status_key,self.priority,self.created)
|
||||
|
||||
@property
|
||||
def age(self):
|
||||
dt=datetime.today()-self.created
|
||||
return dt - dt % timedelta(seconds=1)
|
||||
|
||||
@classmethod
|
||||
def next(cls):
|
||||
for job in sorted(cls.query.all(),key=lambda v:v.sort_key):
|
||||
if job.status[1] in ['Done']:
|
||||
continue
|
||||
return job
|
||||
return None
|
||||
# return cls.query.
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
states=[
|
||||
("primary", "Done"),
|
||||
("danger", "Error"),
|
||||
("info", "Stalled"),
|
||||
("success", "Running"),
|
||||
("secondary", "Starting"),
|
||||
("warning", "Queued")
|
||||
]
|
||||
#return states[self.id.int%len(states)]
|
||||
if self.state.get("result"):
|
||||
return ("primary", "Done")
|
||||
if self.state.get("error"):
|
||||
return ("danger", "Error")
|
||||
if self.state.get("progress"):
|
||||
if (datetime.today() - self.last_update).total_seconds() > (60 * 10):
|
||||
return ("info", "Stalled")
|
||||
return ("success", "Running")
|
||||
if self.started is not None:
|
||||
return ("secondary", "Starting")
|
||||
return ("warning", "Queued")
|
||||
|
||||
@status.setter
|
||||
def __set_status(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"args": self.args,
|
||||
"kwargs": self.kwargs,
|
||||
"state": self.state,
|
||||
"finished": self.finished,
|
||||
"created": self.created,
|
||||
"started": self.started,
|
||||
}
|
||||
|
||||
@dict.setter
|
||||
def __set_dict(self, value):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def route(self):
|
||||
try:
|
||||
return prepare_route(self.state["result"])
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def t_rem(self):
|
||||
if self.started is None:
|
||||
return None
|
||||
runtime = datetime.today() - self.started
|
||||
try:
|
||||
prc_done = self.state["progress"]["prc_done"]
|
||||
if prc_done != 0:
|
||||
t_rem = (runtime / prc_done) * (100 - prc_done)
|
||||
return timedelta(seconds=round(t_rem.total_seconds(), 0))
|
||||
return None
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@t_rem.setter
|
||||
def __set_t_rem(self, value):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def new(cls, func, args=None, kwargs=None):
|
||||
args = args or ()
|
||||
kwargs = kwargs or {}
|
||||
job = cls(args=args, kwargs=kwargs, func=func.__qualname__)
|
||||
job.__last_upd = 0.0
|
||||
if current_user.is_authenticated:
|
||||
job.user = current_user
|
||||
db.session.add(job)
|
||||
db.session.commit()
|
||||
return job
|
||||
|
||||
def start(self):
|
||||
global executor
|
||||
self.state = {}
|
||||
self.started = None
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
args = self.args + [self.callback]
|
||||
try:
|
||||
future = executor.submit_stored(self.id, compute_route, args, self.kwargs)
|
||||
except (BrokenProcessPool, RuntimeError) as e:
|
||||
print("Error:", e)
|
||||
print("Restarting Executor!")
|
||||
executor = Executor(app)
|
||||
future = executor.submit_stored(self.id, compute_route, args, self.kwargs)
|
||||
future.add_done_callback(self.done)
|
||||
|
||||
def callback(self, cb_state):
|
||||
try:
|
||||
if self.started is None:
|
||||
self.started = datetime.today()
|
||||
if self.last_update is not None:
|
||||
time_since_last_upd = (
|
||||
datetime.today() - self.last_update
|
||||
).total_seconds()
|
||||
if time_since_last_upd < 5.0:
|
||||
return
|
||||
state = dict()
|
||||
state.update(self.state)
|
||||
state.update({"progress": cb_state})
|
||||
self.state = state
|
||||
self.last_update = datetime.today()
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def done(self, future):
|
||||
print(self.id, "DONE")
|
||||
state = dict()
|
||||
state.update(self.state)
|
||||
executor.futures.pop(self.id)
|
||||
exc = future.exception()
|
||||
if exc:
|
||||
state.update(
|
||||
{"error": {"type": type(exc).__name__, "args": list(exc.args)}}
|
||||
)
|
||||
else:
|
||||
state.update({"result": future.result()})
|
||||
self.state = state
|
||||
self.finished = datetime.now()
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
db.create_all()
|
||||
for role in ['admin','user','worker_host']:
|
||||
if Role.query.filter_by(name=role).one_or_none() is None:
|
||||
db.session.add(Role(role))
|
||||
|
||||
def create_user(name,password,roles,active=False):
|
||||
user=User.query.filter_by(name=name).one_or_none()
|
||||
if user:
|
||||
db.session.delete(user)
|
||||
user=User(name=name,password=password,is_active=active)
|
||||
user.add_roles(roles)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user
|
||||
|
||||
create_user('admin','admin',['admin','user'],True)
|
||||
create_user('user','user',['user'],True)
|
||||
create_user('host','host',['user','worker_host'],True)
|
||||
|
||||
|
||||
|
||||
class SQLAView(ModelView):
|
||||
column_exclude_list = ["password"]
|
||||
column_editable_list = []
|
||||
create_modal = True
|
||||
edit_modal = True
|
||||
can_view_details = True
|
||||
column_display_pk = True
|
||||
|
||||
def is_accessible(self):
|
||||
return current_user.is_authenticated and current_user.has_role('admin')
|
||||
|
||||
def inaccessible_callback(self, name, **kwargs):
|
||||
return redirect(url_for("login"))
|
||||
|
||||
|
||||
class UserView(SQLAView):
|
||||
from wtforms import PasswordField
|
||||
|
||||
column_list = ("name", "active", "password", "api_key","roles")
|
||||
column_formatters = {
|
||||
"password": lambda view, context, model, name: "",
|
||||
"api_key": lambda view, context, model, name: model.api_key or "",
|
||||
}
|
||||
form_extra_fields = {"password": PasswordField("Password")}
|
||||
|
||||
|
||||
class JobView(SQLAView):
|
||||
# Job.id,Job.user,Job.func,Job.args,Job.kwargs,Job.state,Job.created,Job.finished,Job.started,Job.last_update
|
||||
column_list = ("id", "status", "user", "created", "started", "finished")
|
||||
column_formatters = {
|
||||
"status": lambda view, context, model, name: model.status[1],
|
||||
}
|
||||
|
||||
|
||||
class WorkerView(SQLAView):
|
||||
pass
|
||||
# # Job.id,Job.user,Job.func,Job.args,Job.kwargs,Job.state,Job.created,Job.finished,Job.started,Job.last_update
|
||||
# column_list = ("id", "status", "user", "created", "started", "finished")
|
||||
# column_formatters = {
|
||||
# "user": lambda view, context, model, name: model.user.name
|
||||
# if model.user
|
||||
# else "",
|
||||
# "status": lambda view, context, model, name: model.status[1],
|
||||
# }
|
||||
|
||||
admin.add_view(JobView(Job, db.session))
|
||||
admin.add_view(UserView(User, db.session))
|
||||
admin.add_view(SQLAView(Worker, db.session))
|
||||
admin.add_view(SQLAView(Role, db.session))
|
||||
|
||||
|
||||
def submit_job(func, *args, **kwargs):
|
||||
job = Job.new(func, args, kwargs)
|
||||
job.start()
|
||||
return job.id
|
||||
|
||||
|
||||
@app.route("/api/route", methods=["GET", "POST"])
|
||||
@use_kwargs(
|
||||
{
|
||||
"jump_range": fields.Float(required=True),
|
||||
"mode": fields.String(
|
||||
missing="bfs", validate=validate.OneOf(["bfs", "greedy", "a-star"])
|
||||
),
|
||||
"systems": fields.DelimitedList(fields.String, required=True),
|
||||
"permute": fields.String(
|
||||
missing=None,
|
||||
validate=validate.OneOf(
|
||||
["off", "all", "keep_first", "keep_last", "keep_both"]
|
||||
),
|
||||
),
|
||||
"primary": fields.Boolean(missing=False),
|
||||
"factor": fields.Float(missing=0.5),
|
||||
}
|
||||
)
|
||||
def api_route(_=None, **args):
|
||||
if args["permute"] == "off":
|
||||
args["permute"] = None
|
||||
args["systems"] = [s.strip() for s in args["systems"]]
|
||||
args = (
|
||||
args["systems"],
|
||||
args["jump_range"],
|
||||
None,
|
||||
args["mode"],
|
||||
args["primary"],
|
||||
args["permute"] is not None,
|
||||
args["permute"] in ["keep_first", "keep_both"],
|
||||
args["permute"] in ["keep_last", "keep_both"],
|
||||
args["factor"],
|
||||
None,
|
||||
r"D:\devel\rust\ED_LRR\stars.csv",
|
||||
app.config['ROUTE_WORKERS']
|
||||
)
|
||||
return jsonify({"id": submit_job(ed_lrr.route, *args)})
|
||||
|
||||
|
||||
@app.route("/api/status")
|
||||
def api_status():
|
||||
info = {"queued_jobs": len(executor.futures._futures)}
|
||||
return jsonify(info)
|
||||
|
||||
|
||||
@app.route("/api/whoami")
|
||||
def api_whoami():
|
||||
return jsonify({'name':current_user.name})
|
||||
|
||||
|
||||
@app.route("/api/status/<uuid:job_id>")
|
||||
def api_job_status(job_id):
|
||||
job = Job.query.get_or_404(str(job_id))
|
||||
return jsonify(job.dict)
|
||||
|
||||
|
||||
@app.route("/static/<path:path>")
|
||||
def send_static(path):
|
||||
return send_from_directory("static", path)
|
||||
|
||||
|
||||
@app.route("/route", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def route():
|
||||
form = RouteForm()
|
||||
if form.validate_on_submit():
|
||||
data = dict(form.data)
|
||||
if data["permute"] == "off":
|
||||
data["permute"] = None
|
||||
del data["csrf_token"]
|
||||
del data["submit"]
|
||||
job = api_route(data)
|
||||
return redirect(url_for("status", job_id=job.json["id"]))
|
||||
return render_template("form.html", form=form, title="Plot Route")
|
||||
|
||||
|
||||
@app.route("/status/",defaults={'job_id':None})
|
||||
@app.route("/status/<uuid:job_id>")
|
||||
@login_required
|
||||
def status(job_id=None):
|
||||
if job_id is not None:
|
||||
job=Job.query.get_or_404(str(job_id))
|
||||
return render_template("job.html", job=job)
|
||||
return render_template(
|
||||
"status.html", Job=Job, state=request.args.get("state")
|
||||
)
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
|
||||
@app.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("index"))
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(name=form.data["username"]).one_or_none()
|
||||
if (user is None) or (user.password != form.data["password"]):
|
||||
flash("Invalid credentials!", "danger")
|
||||
return redirect(url_for("login"))
|
||||
if not user.is_active:
|
||||
flash("Account is deactivated!", "warning")
|
||||
return redirect(url_for("login"))
|
||||
login_user(user, remember=form.data["remember"])
|
||||
next = request.args.get('next')
|
||||
if not is_safe_url(next):
|
||||
next=None
|
||||
return redirect(next or url_for("status"))
|
||||
return render_template("form.html", form=form, title="Login")
|
||||
|
||||
|
||||
@app.route("/register", methods=["GET", "POST"])
|
||||
def register():
|
||||
form = RegisterForm()
|
||||
if form.validate_on_submit():
|
||||
if User.query.filter_by(name=form.data["username"]).one_or_none() is not None:
|
||||
flash('Username already exists','danger')
|
||||
return render_template("form.html", form=form, title="Register")
|
||||
user = User()
|
||||
user.name = form.data["username"]
|
||||
user.password = form.data["password"]
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
login_user(user)
|
||||
return redirect(url_for("status"))
|
||||
return render_template("form.html", form=form, title="Register")
|
||||
|
||||
|
||||
@app.route("/change_password", methods=["GET", "POST"])
|
||||
def change_password():
|
||||
if current_user.is_anonymous:
|
||||
return redirect(url_for("index"))
|
||||
form = ChangePasswordForm()
|
||||
if form.validate_on_submit():
|
||||
if form.data["old_password"] == current_user.password:
|
||||
current_user.password = form.data["password"]
|
||||
flash("Password changed!", "success")
|
||||
else:
|
||||
flash("Wrong password!", "danger")
|
||||
return render_template("form.html", form=form, title="Register")
|
||||
return redirect(url_for("status"))
|
||||
return render_template("form.html", form=form, title="Register")
|
||||
|
||||
@app.route("/workers/")
|
||||
@login_required
|
||||
def worker():
|
||||
return render_template("workers.html")
|
||||
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for("login"))
|
||||
|
||||
|
||||
@app.before_first_request
|
||||
def resume_jobs():
|
||||
print(Job.next())
|
||||
with app.test_request_context():
|
||||
for job in Job.query.all():
|
||||
if job.status[1] != "Done":
|
||||
print("Restarting {} with state {}".format(job.id, job.status[1]))
|
||||
job.start()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="127.0.0.1", port=3777, debug=True)
|
18
ed_lrr_gui/web/config.py
Normal file
18
ed_lrr_gui/web/config.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
import os
|
||||
|
||||
SECRET_KEY = "ED_LRR_WEBAPP"
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = "sqlite:///ed_lrr_web_ui.db"
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
ROUTE_WORKERS = 0
|
||||
|
||||
EXECUTOR_TYPE = "process"
|
||||
EXECUTOR_MAX_WORKERS = os.cpu_count()-1
|
||||
EXECUTOR_FUTURES_MAX_LENGTH = 500
|
||||
|
||||
FLASK_ADMIN_SWATCH = "Darkly"
|
||||
|
||||
DEBUG_TB_TEMPLATE_EDITOR_ENABLED = True
|
||||
|
||||
MAIL_DEFAULT_SENDER = '"ED_LRR Admin" <ed_lrr@gmail.com>'
|
104
ed_lrr_gui/web/forms.py
Normal file
104
ed_lrr_gui/web/forms.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import (
|
||||
StringField,
|
||||
PasswordField,
|
||||
FieldList,
|
||||
FloatField,
|
||||
BooleanField,
|
||||
SelectField,
|
||||
SubmitField,
|
||||
validators,
|
||||
Field,
|
||||
)
|
||||
from wtforms.widgets.html5 import NumberInput
|
||||
from wtforms.widgets import TextInput
|
||||
from wtforms.validators import ValidationError
|
||||
|
||||
class StringListField(Field):
|
||||
widget = TextInput()
|
||||
|
||||
def _value(self):
|
||||
if self.data:
|
||||
return u",".join(self.data)
|
||||
else:
|
||||
return u""
|
||||
|
||||
def process_formdata(self, valuelist):
|
||||
if valuelist:
|
||||
self.data = [x.strip() for x in valuelist[0].split(",")]
|
||||
else:
|
||||
self.data = []
|
||||
|
||||
|
||||
class RouteForm(FlaskForm):
|
||||
systems = StringListField("Systems", [validators.DataRequired()])
|
||||
jump_range = FloatField(
|
||||
"Jump Range (Ly)",
|
||||
[validators.DataRequired(), validators.NumberRange(0, None)],
|
||||
widget=NumberInput(min=0, step=0.1),
|
||||
)
|
||||
mode = SelectField(
|
||||
"Routing Mode",
|
||||
choices=[
|
||||
("bfs", "Breadth-First Search"),
|
||||
("greedy", "Greedy Search"),
|
||||
("a-star", "A*-Search"),
|
||||
],
|
||||
)
|
||||
permute = SelectField(
|
||||
"Permutation Mode",
|
||||
choices=[
|
||||
("off", "Off"),
|
||||
("keep_first", "Keep starting system"),
|
||||
("keep_last", "Keep destination system"),
|
||||
("keep_both", "Keep both endpoints"),
|
||||
],
|
||||
)
|
||||
primary = BooleanField("Only route through primary stars")
|
||||
factor = FloatField(
|
||||
"Greedyness for A*-Search (%)",
|
||||
[validators.NumberRange(0, 100)],
|
||||
default=50,
|
||||
widget=NumberInput(min=0, max=100, step=1),
|
||||
)
|
||||
|
||||
priority = FloatField(
|
||||
"Priority (0=max, 100=min)",
|
||||
[validators.NumberRange(0, 100)],
|
||||
default=0,
|
||||
widget=NumberInput(min=0, max=100, step=1),
|
||||
)
|
||||
submit = SubmitField("GO!")
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField("Username", [validators.Required()])
|
||||
password = PasswordField("Password", [validators.Required()])
|
||||
remember = BooleanField("Remember me")
|
||||
submit = SubmitField("Login")
|
||||
|
||||
|
||||
class RegisterForm(FlaskForm):
|
||||
username = StringField("Username", [validators.Required()])
|
||||
password = PasswordField(
|
||||
"Password",
|
||||
[
|
||||
validators.Required(),
|
||||
validators.EqualTo("confirm", message="Passwords must match"),
|
||||
],
|
||||
)
|
||||
confirm = PasswordField("Verify password", [validators.Required()])
|
||||
submit = SubmitField("Login")
|
||||
|
||||
|
||||
class ChangePasswordForm(FlaskForm):
|
||||
old_password = PasswordField("Current Password", [validators.Required()])
|
||||
password = PasswordField(
|
||||
"Password",
|
||||
[
|
||||
validators.Required(),
|
||||
validators.EqualTo("confirm", message="Passwords must match"),
|
||||
],
|
||||
)
|
||||
confirm = PasswordField("Verify password", [validators.Required()])
|
||||
submit = SubmitField("Change")
|
23
ed_lrr_gui/web/static/theme.css
Normal file
23
ed_lrr_gui/web/static/theme.css
Normal file
|
@ -0,0 +1,23 @@
|
|||
body,input,select,pre {
|
||||
background-color: #222 !important;
|
||||
color: #eee;
|
||||
}
|
||||
table {
|
||||
line-height: 1;
|
||||
}
|
||||
.progress {
|
||||
background-color: #444;
|
||||
}
|
||||
.progress-bar {
|
||||
background-color: #f70;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
color: #eee !important;
|
||||
}
|
||||
|
||||
#graph {
|
||||
border: 1px solid #eee;
|
||||
width: 512px;
|
||||
height: 512px;
|
||||
}
|
5
ed_lrr_gui/web/templates/admin/index.html
Normal file
5
ed_lrr_gui/web/templates/admin/index.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
{% extends 'admin/master.html' %}
|
||||
|
||||
{% block body %}
|
||||
<p>Hello world</p>
|
||||
{% endblock %}
|
48
ed_lrr_gui/web/templates/base.html
Normal file
48
ed_lrr_gui/web/templates/base.html
Normal file
|
@ -0,0 +1,48 @@
|
|||
{% extends "bootstrap/base.html" %}
|
||||
{% import "bootstrap/utils.html" as utils %}
|
||||
{% block title %}Elite: Dangerous Long Range Router{% endblock %}
|
||||
|
||||
{% block scrips %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='theme.css')}}">
|
||||
{% endblock %}
|
||||
|
||||
{% block navbar %}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark" style="background-color: #222;">
|
||||
<a class="navbar-brand" href="/">E:D LRR</a>
|
||||
<ul class="navbar-nav mr-auto">
|
||||
{{nav.left_nav.render(renderer='bootstrap4')}}
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav ml-auto">
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link">
|
||||
Logged in as {{current_user.name}}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{{nav.right_nav.render(renderer='bootstrap4')}}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category,message in messages %}
|
||||
<div class="alert alert-{{category}}" role="{{category}}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{# application content needs to be provided in the app_content block #}
|
||||
{% block app_content %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
6
ed_lrr_gui/web/templates/error/404.html
Normal file
6
ed_lrr_gui/web/templates/error/404.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>404 Not Found</h1>
|
||||
<p><a href="{{ url_for('index') }}"><button type="button" class="btn btn-secondary">Back</button></a></p>
|
||||
{% endblock %}
|
16
ed_lrr_gui/web/templates/form.html
Normal file
16
ed_lrr_gui/web/templates/form.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "bootstrap/wtf.html" as wtf %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>{{title}}</h1>
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<div class="alert alert-danger" role="danger">{{error}}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{{ wtf.quick_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
10
ed_lrr_gui/web/templates/index.html
Normal file
10
ed_lrr_gui/web/templates/index.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>E:D LRR</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
Number of Jobs: {{current_user.jobs|count}}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
131
ed_lrr_gui/web/templates/job.html
Normal file
131
ed_lrr_gui/web/templates/job.html
Normal file
|
@ -0,0 +1,131 @@
|
|||
{% extends "base.html" %}
|
||||
{% block app_content %}
|
||||
<h1>Job Status <span class="badge badge-{{job.status[0]}}">{{ job.status[1] }}</span></h1>
|
||||
<div class="row">
|
||||
<div class="col-lg-0">
|
||||
{% if job.state.error %}
|
||||
<ul>
|
||||
{% for err in job.state.error.args %}
|
||||
<li>{{err}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if job.state.progress %}
|
||||
<p class="lead">Routing from <b>{{ job.state.progress.from }}</b> to <b>{{ job.state.progress.to }}</b> using
|
||||
{{ job.state.progress.mode }}</p>
|
||||
<p>Current system: <b>{{ job.state.progress.system }}</b></p>
|
||||
<p>Search queue size: <b>{{"{:,}".format(job.state.progress.queue_size) }}</b></p>
|
||||
<p>Number of systems checked: <b>{{"{:,}".format(job.state.progress.n_seen) }}
|
||||
({{job.state.progress.prc_seen|round(2)}} %)</b></p>
|
||||
<p>Estimated time remaining: <b>{{job.t_rem}}</b></p>
|
||||
<p>Search Depth: <b>{{job.state.progress.depth}}</b></p>
|
||||
<div class="progress" style="width: 100%;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
style="width: {{job.state.progress.prc_done}}%;" role="progressbar"
|
||||
aria-valuenow="{{job.state.progress.prc_done|round(2)}}" aria-valuemin="0" aria-valuemax="100">
|
||||
{{job.state.progress.prc_done|round(2)}} %
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if job.state.result %}
|
||||
<h2>Result</h2>
|
||||
|
||||
<h3>Map</h3>
|
||||
<div id="graph">
|
||||
</div>
|
||||
<script src="https://d3js.org/d3.v5.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
function dist(a, b) {
|
||||
var sum = 0;
|
||||
for (var i = 0; i < a.length; ++i) {
|
||||
sum += Math.pow(a[i] - b[i], 2)
|
||||
}
|
||||
return Math.pow(sum, 0.5);
|
||||
}
|
||||
var width = 512;
|
||||
var height = 512;
|
||||
var route = {{job.route | tojson}};
|
||||
var vis = d3.select("#graph")
|
||||
.append("svg").attr("viewBox", [0, 0, width, height]);
|
||||
|
||||
vis.attr("width", width)
|
||||
.attr("height", height);
|
||||
var g = vis.append("g");
|
||||
|
||||
vis.call(d3.zoom()
|
||||
.extent([
|
||||
[0, 0],
|
||||
[width, height]
|
||||
])
|
||||
.on("zoom", () => {
|
||||
g.attr("transform", d3.event.transform);
|
||||
}));
|
||||
|
||||
var lines = [];
|
||||
for (var i = 0; i < route.length - 1; ++i) {
|
||||
lines.push({
|
||||
x1: route[i].pos[1],
|
||||
x2: route[i + 1].pos[1],
|
||||
y1: -route[i].pos[2],
|
||||
y2: -route[i + 1].pos[2],
|
||||
dist: dist(route[i].pos, route[i + 1].pos),
|
||||
color: route[i].color || '#eee'
|
||||
})
|
||||
}
|
||||
|
||||
g.selectAll(".line")
|
||||
.data(lines)
|
||||
.enter()
|
||||
.append("line")
|
||||
.attr("x1", (l) => l.x1)
|
||||
.attr("y1", (l) => l.y1)
|
||||
.attr("x2", (l) => l.x2)
|
||||
.attr("y2", (l) => l.y2)
|
||||
.style("stroke", (l) => l.color)
|
||||
.style("stroke-width", 5)
|
||||
.append("title")
|
||||
.text((l) => Math.round(l.dist * 100) / 100 + " Ly");
|
||||
|
||||
g.selectAll("circle .nodes")
|
||||
.data(route)
|
||||
.enter()
|
||||
.append("svg:circle")
|
||||
.attr("class", "nodes")
|
||||
.attr("cx", (d) => d.pos[1])
|
||||
.attr("cy", (d) => -d.pos[2])
|
||||
.attr("r", 10)
|
||||
.attr("fill", (d) => d.color)
|
||||
.append("title")
|
||||
.text((d) => d.body + " (" + d.star_type + ")")
|
||||
</script>
|
||||
<h3>Jumps</h3>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Num</th>
|
||||
<th scope="col">Body</th>
|
||||
<th scope="col">Type</th>
|
||||
<th scope="col">Distance from arrival</th>
|
||||
<th scope="col">Jump distance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for sys in job.route %}
|
||||
<tr>
|
||||
<th scope="row">{{sys.num}}</td>
|
||||
<td>{{sys.body}}</td>
|
||||
<td style="color: {{sys.color}}">{{sys.star_type}}</td>
|
||||
<td>{{"{:,}".format(sys.distance)}} Ls</td>
|
||||
<td>{{"{:,}".format(sys.jump_dist|round(2))}} Ly</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<th scope="row" colspan=4>Total Distance</th>
|
||||
<td>{{"{:,}".format(job.route|sum(attribute='jump_dist')|round(2))}} Ly</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
87
ed_lrr_gui/web/templates/status.html
Normal file
87
ed_lrr_gui/web/templates/status.html
Normal file
|
@ -0,0 +1,87 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block app_content %}
|
||||
{% if current_user.has_role('admin') %}
|
||||
{% set jobs = Job.query.all() %}
|
||||
{% else %}
|
||||
{% set jobs = current_user.jobs %}
|
||||
{% endif %}
|
||||
<h1>System Status</h1>
|
||||
<div class="row">
|
||||
<h2>Overview</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table table-striped table-bordered" style="width: 1px;">
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
{% for group in (jobs|groupby('status')) %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="?state={{group.grouper[1]}}">
|
||||
<span class="badge badge-{{ group.grouper[0] }}">{{ group.grouper[1] }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{group.list|count}}
|
||||
</td>
|
||||
<tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="?">
|
||||
Total
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{jobs|count}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h2>Jobs</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<!-- Next: {{Job.next().id}} -->
|
||||
<table class="table table-striped table-bordered" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">Systems</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">User</th>
|
||||
<th scope="col">Priority</th>
|
||||
<th scope="col">Progess</th>
|
||||
<th scope="col">ETC</th>
|
||||
<th scope="col">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for job in (jobs|sort(attribute='sort_key')) %}
|
||||
{% if (state==None) or job.status[1]==state %}
|
||||
<tr>
|
||||
<td style="width: 1px; white-space: nowrap;"><a href="{{url_for('status',job_id=job.id)}}">{{job.id}}</a></td>
|
||||
<td style="width: 1px; white-space: nowrap;">{{job.args[0]|join(', ')}}</td>
|
||||
<td style="width: 1px; white-space: nowrap;"><span class="badge badge-{{job.status[0]}}">{{ job.status[1] }}</span></td>
|
||||
<td style="width: 1px; white-space: nowrap;">{{job.user.name}}</td>
|
||||
<td style="width: 1px; white-space: nowrap;">{{job.priority}}</td>
|
||||
{% if job.state.progress %}
|
||||
<td>
|
||||
<div class="progress" style="width: 100%;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: {{job.state.progress.prc_done}}%;" role="progressbar" aria-valuenow="{{job.state.progress.prc_done|round(2)}}" aria-valuemin="0" aria-valuemax="100">
|
||||
{{job.state.progress.prc_done|round(2)}} %
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>Unknown</td>
|
||||
{% endif %}
|
||||
<td style="width: 1px; white-space: nowrap;">{{job.t_rem}}</td>
|
||||
<td style="width: 1px; white-space: nowrap;">{{job.age}} ago</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
14
ed_lrr_gui/web/templates/workers.html
Normal file
14
ed_lrr_gui/web/templates/workers.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>Workers</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{% if current_user.is_authenticated %}
|
||||
Hello {{current_user.name}}!
|
||||
{% else %}
|
||||
Nothing to see here!
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
73
ed_lrr_gui/web/utils.py
Normal file
73
ed_lrr_gui/web/utils.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
from flask_nav.renderers import Renderer
|
||||
from dominate import tags
|
||||
from urllib.parse import urlparse,urljoin
|
||||
from flask import request
|
||||
|
||||
def is_safe_url(target):
|
||||
ref_url = urlparse(request.host_url)
|
||||
test_url = urlparse(urljoin(request.host_url, target))
|
||||
return test_url.scheme in ('http', 'https') and \
|
||||
ref_url.netloc == test_url.netloc
|
||||
|
||||
def dist(p1, p2):
|
||||
s = 0
|
||||
for c1, c2 in zip(p1, p2):
|
||||
s += (c1 - c2) ** 2
|
||||
return s ** 0.5
|
||||
|
||||
|
||||
class BootsrapRenderer(Renderer):
|
||||
def visit_Navbar(self, node):
|
||||
sub = []
|
||||
for item in node.items:
|
||||
sub.append(self.visit(item))
|
||||
return "".join([v.render() for v in sub])
|
||||
|
||||
def visit_View(self, node):
|
||||
classes = ["nav-link"]
|
||||
if node.active:
|
||||
classes.append("active")
|
||||
return tags.li(
|
||||
tags.a(node.text, href=node.get_url(), cls=" ".join(classes)),
|
||||
cls="nav-item",
|
||||
)
|
||||
|
||||
def visit_Subgroup(self, node):
|
||||
# almost the same as visit_Navbar, but written a bit more concise
|
||||
return tags.div(node.title, *[self.visit(item) for item in node.items])
|
||||
|
||||
|
||||
colors = {
|
||||
"O": "#0000FF",
|
||||
"B": "#140AF0",
|
||||
"A": "#3C1EDC",
|
||||
"F": "#EEEEEE",
|
||||
"G": "#969646",
|
||||
"K": "#B43C1E",
|
||||
"M": "#FF280A",
|
||||
"L": "#FF1E00",
|
||||
"T": "#800000",
|
||||
"Y": "#800000",
|
||||
"White Dwarf": "#5D67EF",
|
||||
"Neutron": "#99A0FF",
|
||||
}
|
||||
|
||||
|
||||
def prepare_route(route):
|
||||
entries = []
|
||||
prev = route[0]
|
||||
num = 1
|
||||
for hop in route[1:]:
|
||||
prev["jump_dist"] = dist(hop["pos"], prev["pos"])
|
||||
prev["num"] = num
|
||||
prev["color"] = colors.get(prev["star_type"].split()[0], "#eee")
|
||||
prev["distance"] = prev["distance"]
|
||||
entries.append(prev)
|
||||
prev = hop
|
||||
num += 1
|
||||
prev["jump_dist"] = 0
|
||||
prev["distance"] = prev["distance"]
|
||||
prev["num"] = num
|
||||
prev["color"] = colors.get(prev["star_type"].split()[0], "#eee")
|
||||
entries.append(prev)
|
||||
return entries
|
6
ed_lrr_gui/web/worker.py
Normal file
6
ed_lrr_gui/web/worker.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
import requests as RQ
|
||||
import _ed_lrr as ed_lrr
|
||||
|
||||
funcs = {
|
||||
func: getattr(ed_lrr, func) for func in dir(ed_lrr) if not func.startswith("_")
|
||||
}
|
68
rust/src/galaxy.rs
Normal file
68
rust/src/galaxy.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use serde::Deserialize;
|
||||
use serde_json::Result;
|
||||
use serde_json;
|
||||
use std::fs::File;
|
||||
use std::io::Seek;
|
||||
use std::io::{BufRead, BufReader, BufWriter, SeekFrom};
|
||||
use std::path::PathBuf;
|
||||
use std::str;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Coords {
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Body {
|
||||
name: String,
|
||||
#[serde(rename = "type")]
|
||||
body_type: String,
|
||||
subType: String,
|
||||
#[serde(rename = "distanceToArrival")]
|
||||
distance: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct System {
|
||||
coords: Coords,
|
||||
name: String,
|
||||
bodies: Vec<Body>,
|
||||
}
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
better_panic::install();
|
||||
let mut buffer = String::new();
|
||||
let mut bz2_reader = std::process::Command::new("bzip2").args(
|
||||
&["-d","-c",r#"E:\EDSM\galaxy.json.bz2"#]
|
||||
).stdout(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.expect("Failed to spawn execute bzip2!");
|
||||
let mut reader = BufReader::new(bz2_reader.stdout.as_mut().expect("Failed to open stdout of child process"));
|
||||
let mut count = 0;
|
||||
while let Ok(n) = reader.read_line(&mut buffer) {
|
||||
if n==0 {
|
||||
break;
|
||||
}
|
||||
buffer = buffer
|
||||
.trim()
|
||||
.trim_end_matches(|c| c == ',')
|
||||
.trim()
|
||||
.to_string();
|
||||
if let Ok(sys) = serde_json::from_str::<System>(&buffer) {
|
||||
for b in &sys.bodies {
|
||||
if b.body_type == "Star" {
|
||||
count += 1;
|
||||
if (count % 100_000) == 0 {
|
||||
println!("{}: {:?}", count, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer.clear();
|
||||
}
|
||||
println!("Total: {}", count);
|
||||
Ok(())
|
||||
}
|
24
tests/test_ed_lrr.py
Normal file
24
tests/test_ed_lrr.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
import pytest
|
||||
|
||||
stars_csv = "D:\\devel\\rust\\ED_LRR\\stars.csv"
|
||||
|
||||
|
||||
@pytest.mark.dependency()
|
||||
def test_import():
|
||||
import _ed_lrr
|
||||
|
||||
|
||||
@pytest.mark.dependency(depends=["test_import"])
|
||||
def test_search_works():
|
||||
import _ed_lrr
|
||||
|
||||
system_names = ["Ix", "Sol", "Colonia", "Sagittarius A*"]
|
||||
systems = _ed_lrr.find_sys(system_names, stars_csv)
|
||||
print(systems)
|
||||
|
||||
|
||||
@pytest.mark.dependency(depends=["test_import"])
|
||||
def test_zero_range_fails():
|
||||
import _ed_lrr
|
||||
|
||||
# _ed_lrr.route()
|
18
tests/test_gui.py
Normal file
18
tests/test_gui.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.dependency()
|
||||
def test_import():
|
||||
import ed_lrr_gui
|
||||
from ed_lrr_gui.main import main
|
||||
import ed_lrr_gui.gui as ED_LRR_GUI
|
||||
|
||||
|
||||
@pytest.mark.dependency(depends=["test_import"])
|
||||
def test_search_works():
|
||||
import ed_lrr_gui
|
||||
|
||||
|
||||
@pytest.mark.dependency(depends=["test_import"])
|
||||
def test_zero_range_fails():
|
||||
import ed_lrr_gui
|
Loading…
Reference in a new issue