Skip to content

Commit b7979a5

Browse files
committed
feat: basic DCOR support
1 parent 805f8fe commit b7979a5

File tree

10 files changed

+486
-41
lines changed

10 files changed

+486
-41
lines changed

CHANGELOG

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
2.0.4
1+
2.1.0
2+
- feat: basic DCOR support
23
- enh: export event image via context menu in Quick View (#35)
34
2.0.3
45
- setup: bump dclab from 0.20.3 to 0.21.1

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"numpy>=1.9.0",
3232
"pyqt5",
3333
"pyqtgraph>=0.10.0",
34+
"requests",
3435
"scipy>=0.13.0"],
3536
python_requires='>=3.6, <4',
3637
setup_requires=['pytest-runner'],

shapeout2/gui/dcor/__init__.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import pkg_resources
2+
import requests
3+
import urllib.parse
4+
5+
import dclab
6+
from PyQt5 import uic, QtWidgets
7+
8+
from ... import settings
9+
10+
11+
class DCORLoader(QtWidgets.QDialog):
12+
def __init__(self, parent, *args, **kwargs):
13+
QtWidgets.QWidget.__init__(self, parent, *args, **kwargs)
14+
path_ui = pkg_resources.resource_filename(
15+
"shapeout2.gui.dcor", "dcor.ui")
16+
uic.loadUi(path_ui, self)
17+
18+
self.main_ui = parent
19+
self.search_results = []
20+
21+
# Update UI
22+
self.settings = settings.SettingsFile()
23+
24+
# hide SSL checkbox
25+
if not self.settings.get_bool("developer mode"):
26+
self.checkBox_ssl.hide()
27+
self._init_combobox()
28+
self.lineEdit_api_key.setText(self.settings.get_string("dcor api key"))
29+
30+
# tool button
31+
self.pushButton_search.clicked.connect(self.on_search)
32+
self.pushButton_search.setDefault(True)
33+
self.buttonBox.buttons()[1].setDefault(False)
34+
self.buttonBox.buttons()[0].setDefault(False)
35+
self.lineEdit_search.setFocus()
36+
37+
def _init_combobox(self):
38+
# update server list
39+
servs = self.settings.get_string_list("dcor servers")
40+
self.comboBox_server.clear()
41+
self.comboBox_server.addItems(servs)
42+
self.comboBox_server.setCurrentIndex(0)
43+
44+
def done(self, r):
45+
if r:
46+
for ii in range(self.listWidget.count()):
47+
item = self.listWidget.item(ii)
48+
if item.isSelected():
49+
self.main_ui.add_dataslot(
50+
paths=[self.search_results[ii][0]], is_dcor=True)
51+
super(DCORLoader, self).done(r)
52+
53+
def on_search(self):
54+
# update server list
55+
servs = self.settings.get_string_list("dcor servers")
56+
serv = self.comboBox_server.currentText()
57+
if serv.count("://"):
58+
serv = serv.split("://")[1]
59+
if serv in servs:
60+
# make this server the first choice
61+
servs.remove(serv)
62+
servs = [serv] + servs
63+
api_key = self.lineEdit_api_key.text().strip()
64+
# Add this API Key to the known API Keys (dclab)
65+
dclab.rtdc_dataset.fmt_dcor.APIHandler.add_api_key(api_key)
66+
67+
# save config
68+
self.settings.set_string("dcor api key", api_key)
69+
self.settings.set_string_list("dcor servers", servs)
70+
self._init_combobox()
71+
72+
# ready API
73+
http = "https" if self.checkBox_ssl.isChecked() else "http"
74+
base = "{}://{}/api/3".format(http, serv)
75+
api_headers = {"Authorization": api_key}
76+
77+
# check API availability
78+
req = requests.get(base, headers=api_headers)
79+
if not req.ok:
80+
msg = QtWidgets.QMessageBox()
81+
msg.setIcon(QtWidgets.QMessageBox.Warning)
82+
msg.setText("Failed to connect to DCOR server '{}'! ".format(serv)
83+
+ "Reason: {}".format(req.reason))
84+
msg.setWindowTitle("Connection failed")
85+
msg.exec_()
86+
return
87+
if "version" not in req.json() or req.json()["version"] != 3:
88+
raise ValueError("Invalid response: {}".format(req.json()))
89+
90+
# perform search (limit to 20 results)
91+
if self.comboBox_search.currentIndex() == 1:
92+
stype = "dataset"
93+
else:
94+
stype = "free"
95+
search_string = urllib.parse.quote(self.lineEdit_search.text())
96+
res = self.perform_search(search_string, stype, base, api_headers)
97+
self.listWidget.clear()
98+
for r in res:
99+
self.listWidget.addItem(r[1])
100+
self.search_results = res
101+
102+
def perform_search(self, string, search_type, api_base, api_headers):
103+
"""Perform search
104+
105+
Parameters
106+
----------
107+
string: str
108+
Search string (already parsed using urllib.parse.quote)
109+
search_type: str
110+
"free": free text search
111+
or "dataset": resource/package name/id
112+
api_base: str
113+
Everything up until "https://server.example.org/api/3"
114+
"""
115+
if search_type == "free":
116+
url = api_base + "/action/package_search?q={}".format(string)
117+
# limit to 20 rows
118+
url += "&rows=20"
119+
urls = [url]
120+
else:
121+
urls = [
122+
api_base + "/action/package_show?id={}".format(string),
123+
api_base + "/action/resource_show?id={}".format(string),
124+
]
125+
126+
pkg_res = []
127+
for url in urls:
128+
req = requests.get(url, headers=api_headers)
129+
if not req.ok:
130+
continue
131+
resp = req.json()["result"]
132+
if "count" in resp and "results" in resp:
133+
# free search
134+
for pkg in resp["results"]:
135+
if "resources" in pkg:
136+
for res in pkg["resources"]:
137+
pkg_res.append([pkg, res])
138+
elif "resources" in resp:
139+
# package show
140+
for res in resp["resources"]:
141+
pkg_res.append([resp, res])
142+
else:
143+
# resource show
144+
purl = api_base + "/action/package_show?id={}".format(
145+
resp["package_id"])
146+
pkg = requests.get(purl, headers=api_headers).json()["result"]
147+
pkg_res.append([pkg, resp])
148+
149+
res_list = []
150+
failed = []
151+
for pkg, res in pkg_res:
152+
# check availability of each resource
153+
c = api_base + "/action/dcserv?id={}&query=valid".format(res["id"])
154+
req = requests.get(c, headers=api_headers)
155+
if req.ok:
156+
name = "{}: {} <{}@{}>".format(
157+
pkg["title"],
158+
res["name"],
159+
pkg["name"],
160+
pkg["organization"]["name"],
161+
)
162+
ru = api_base + "/action/dcserv?id={}".format(res["id"])
163+
res_list.append([ru, name])
164+
else:
165+
failed.append("{}: {}".format(res["id"], req.reason))
166+
if failed:
167+
msg = QtWidgets.QMessageBox()
168+
msg.setIcon(QtWidgets.QMessageBox.Information)
169+
msg.setText("Search found invalid data: {}".format(failed))
170+
msg.setWindowTitle("Dataset validation")
171+
msg.exec_()
172+
return res_list

shapeout2/gui/dcor/dcor.ui

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>Dialog</class>
4+
<widget class="QDialog" name="Dialog">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>653</width>
10+
<height>416</height>
11+
</rect>
12+
</property>
13+
<property name="windowTitle">
14+
<string>Load DCOR online data</string>
15+
</property>
16+
<layout class="QVBoxLayout" name="verticalLayout">
17+
<item>
18+
<widget class="QGroupBox" name="groupBox">
19+
<property name="title">
20+
<string>Access settings</string>
21+
</property>
22+
<layout class="QVBoxLayout" name="verticalLayout_2">
23+
<item>
24+
<layout class="QGridLayout" name="gridLayout">
25+
<item row="0" column="1">
26+
<widget class="QComboBox" name="comboBox_server">
27+
<property name="editable">
28+
<bool>true</bool>
29+
</property>
30+
<item>
31+
<property name="text">
32+
<string>dcor.mpl.mpg.de</string>
33+
</property>
34+
</item>
35+
</widget>
36+
</item>
37+
<item row="0" column="0">
38+
<widget class="QLabel" name="label_2">
39+
<property name="text">
40+
<string>Server</string>
41+
</property>
42+
</widget>
43+
</item>
44+
<item row="1" column="0">
45+
<widget class="QLabel" name="label_3">
46+
<property name="text">
47+
<string>API Key</string>
48+
</property>
49+
</widget>
50+
</item>
51+
<item row="1" column="1">
52+
<widget class="QLineEdit" name="lineEdit_api_key">
53+
<property name="text">
54+
<string/>
55+
</property>
56+
<property name="placeholderText">
57+
<string>optional (to access private data)</string>
58+
</property>
59+
</widget>
60+
</item>
61+
</layout>
62+
</item>
63+
<item>
64+
<widget class="QCheckBox" name="checkBox_ssl">
65+
<property name="text">
66+
<string>use SSL</string>
67+
</property>
68+
<property name="checked">
69+
<bool>true</bool>
70+
</property>
71+
</widget>
72+
</item>
73+
</layout>
74+
</widget>
75+
</item>
76+
<item>
77+
<layout class="QHBoxLayout" name="horizontalLayout">
78+
<property name="topMargin">
79+
<number>0</number>
80+
</property>
81+
<item>
82+
<widget class="QComboBox" name="comboBox_search">
83+
<item>
84+
<property name="text">
85+
<string>Free text</string>
86+
</property>
87+
</item>
88+
<item>
89+
<property name="text">
90+
<string>Identifier</string>
91+
</property>
92+
</item>
93+
</widget>
94+
</item>
95+
<item>
96+
<widget class="QLineEdit" name="lineEdit_search">
97+
<property name="placeholderText">
98+
<string/>
99+
</property>
100+
</widget>
101+
</item>
102+
<item>
103+
<widget class="QPushButton" name="pushButton_search">
104+
<property name="text">
105+
<string>Search</string>
106+
</property>
107+
</widget>
108+
</item>
109+
</layout>
110+
</item>
111+
<item>
112+
<widget class="QListWidget" name="listWidget">
113+
<property name="sizeAdjustPolicy">
114+
<enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum>
115+
</property>
116+
<property name="selectionMode">
117+
<enum>QAbstractItemView::MultiSelection</enum>
118+
</property>
119+
<property name="selectionBehavior">
120+
<enum>QAbstractItemView::SelectRows</enum>
121+
</property>
122+
<property name="verticalScrollMode">
123+
<enum>QAbstractItemView::ScrollPerPixel</enum>
124+
</property>
125+
<property name="horizontalScrollMode">
126+
<enum>QAbstractItemView::ScrollPerPixel</enum>
127+
</property>
128+
</widget>
129+
</item>
130+
<item>
131+
<widget class="QDialogButtonBox" name="buttonBox">
132+
<property name="orientation">
133+
<enum>Qt::Horizontal</enum>
134+
</property>
135+
<property name="standardButtons">
136+
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Open</set>
137+
</property>
138+
</widget>
139+
</item>
140+
</layout>
141+
</widget>
142+
<resources/>
143+
<connections>
144+
<connection>
145+
<sender>buttonBox</sender>
146+
<signal>accepted()</signal>
147+
<receiver>Dialog</receiver>
148+
<slot>accept()</slot>
149+
<hints>
150+
<hint type="sourcelabel">
151+
<x>248</x>
152+
<y>254</y>
153+
</hint>
154+
<hint type="destinationlabel">
155+
<x>157</x>
156+
<y>274</y>
157+
</hint>
158+
</hints>
159+
</connection>
160+
<connection>
161+
<sender>buttonBox</sender>
162+
<signal>rejected()</signal>
163+
<receiver>Dialog</receiver>
164+
<slot>reject()</slot>
165+
<hints>
166+
<hint type="sourcelabel">
167+
<x>316</x>
168+
<y>260</y>
169+
</hint>
170+
<hint type="destinationlabel">
171+
<x>286</x>
172+
<y>274</y>
173+
</hint>
174+
</hints>
175+
</connection>
176+
</connections>
177+
</ui>

0 commit comments

Comments
 (0)