vars-localize 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. vars_localize-0.1.0/.gitignore +132 -0
  2. vars_localize-0.1.0/PKG-INFO +48 -0
  3. vars_localize-0.1.0/README.md +37 -0
  4. vars_localize-0.1.0/pyproject.toml +31 -0
  5. vars_localize-0.1.0/src/vars_localize/__init__.py +0 -0
  6. vars_localize-0.1.0/src/vars_localize/__main__.py +34 -0
  7. vars_localize-0.1.0/src/vars_localize/assets/__init__.py +28 -0
  8. vars_localize-0.1.0/src/vars_localize/assets/images/arrow_left.png +0 -0
  9. vars_localize-0.1.0/src/vars_localize/assets/images/arrow_right.png +0 -0
  10. vars_localize-0.1.0/src/vars_localize/ui/AppWindow.py +276 -0
  11. vars_localize-0.1.0/src/vars_localize/ui/BoundingBox.py +243 -0
  12. vars_localize-0.1.0/src/vars_localize/ui/ConceptEntry.py +33 -0
  13. vars_localize-0.1.0/src/vars_localize/ui/ConceptSearchbar.py +29 -0
  14. vars_localize-0.1.0/src/vars_localize/ui/DisplayPanel.py +28 -0
  15. vars_localize-0.1.0/src/vars_localize/ui/EntryTree.py +504 -0
  16. vars_localize-0.1.0/src/vars_localize/ui/ImageView.py +743 -0
  17. vars_localize-0.1.0/src/vars_localize/ui/JSONTree.py +32 -0
  18. vars_localize-0.1.0/src/vars_localize/ui/LoginDialog.py +67 -0
  19. vars_localize-0.1.0/src/vars_localize/ui/Paginator.py +95 -0
  20. vars_localize-0.1.0/src/vars_localize/ui/PropertiesDialog.py +43 -0
  21. vars_localize-0.1.0/src/vars_localize/ui/PropertiesForm.py +66 -0
  22. vars_localize-0.1.0/src/vars_localize/ui/SearchPanel.py +232 -0
  23. vars_localize-0.1.0/src/vars_localize/ui/__init__.py +0 -0
  24. vars_localize-0.1.0/src/vars_localize/util/__init__.py +0 -0
  25. vars_localize-0.1.0/src/vars_localize/util/endpoints.py +115 -0
  26. vars_localize-0.1.0/src/vars_localize/util/m3.py +481 -0
  27. vars_localize-0.1.0/src/vars_localize/util/utils.py +90 -0
@@ -0,0 +1,132 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ # build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+
53
+ # Translations
54
+ *.mo
55
+ *.pot
56
+
57
+ # Django stuff:
58
+ *.log
59
+ local_settings.py
60
+ db.sqlite3
61
+ db.sqlite3-journal
62
+
63
+ # Flask stuff:
64
+ instance/
65
+ .webassets-cache
66
+
67
+ # Scrapy stuff:
68
+ .scrapy
69
+
70
+ # Sphinx documentation
71
+ docs/_build/
72
+
73
+ # PyBuilder
74
+ target/
75
+
76
+ # Jupyter Notebook
77
+ .ipynb_checkpoints
78
+
79
+ # IPython
80
+ profile_default/
81
+ ipython_config.py
82
+
83
+ # pyenv
84
+ .python-version
85
+
86
+ # pipenv
87
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
88
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
89
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
90
+ # install all needed dependencies.
91
+ #Pipfile.lock
92
+
93
+ # celery beat schedule file
94
+ celerybeat-schedule
95
+
96
+ # SageMath parsed files
97
+ *.sage.py
98
+
99
+ # Environments
100
+ .env
101
+ .venv
102
+ env/
103
+ venv/
104
+ ENV/
105
+ env.bak/
106
+ venv.bak/
107
+
108
+ # Spyder project settings
109
+ .spyderproject
110
+ .spyproject
111
+
112
+ # Rope project settings
113
+ .ropeproject
114
+
115
+ # mkdocs documentation
116
+ /site
117
+
118
+ # mypy
119
+ .mypy_cache/
120
+ .dmypy.json
121
+ dmypy.json
122
+
123
+ # Pyre type checker
124
+ .pyre/
125
+
126
+ # PyCharm
127
+ .idea/
128
+
129
+ # Project specific
130
+ cache/
131
+ requirements.lock
132
+ requirements-dev.lock
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.3
2
+ Name: vars-localize
3
+ Version: 0.1.0
4
+ Summary: Tool for creating localization with VARS.
5
+ Author-email: Kevin Barnard <kbarnard@mbari.org>
6
+ Requires-Python: >=3.8
7
+ Requires-Dist: pyqt6>=6.7.1
8
+ Requires-Dist: qdarkstyle>=3.2.3
9
+ Requires-Dist: requests>=2.32.3
10
+ Description-Content-Type: text/markdown
11
+
12
+ # vars-localize
13
+ Tool for creating localizations within the VARS database.
14
+
15
+ Author: Kevin Barnard ([kbarnard@mbari.org](mailto:kbarnard@mbari.org))
16
+
17
+ ## :hammer: Installation
18
+
19
+ > [!NOTE]
20
+ > VARS Localize requires Python 3.8 or later.
21
+
22
+ To install VARS Localize, run:
23
+ ```bash
24
+ pip install vars-localize
25
+ ```
26
+
27
+ ## :rocket: Usage
28
+
29
+ To start the application, run:
30
+ ```bash
31
+ vars-localize [-u URL]
32
+ ```
33
+
34
+ Once the application launches, log in with your VARS username and password.
35
+
36
+ Search for a concept in the bar at the top left of the application, then select a concept from the list of results to populate a tree of imaged moments in the pane below.
37
+ Select an observation from the children in the subtree of the imaged moment, and draw a bounding box around the observed concept by clicking and dragging.
38
+
39
+ You can double-click on any localization to edit its properties in a dialog.
40
+ Additionally, a localization can be resized by dragging the square corners of its bounding box.
41
+
42
+ ## Credits
43
+
44
+ VARS Localize is made with [PyQt6](https://pypi.org/project/PyQt6/).
45
+
46
+ ---
47
+
48
+ Copyright &copy; 2019 [Monterey Bay Aquarium Research Institute](https://www.mbari.org/)
@@ -0,0 +1,37 @@
1
+ # vars-localize
2
+ Tool for creating localizations within the VARS database.
3
+
4
+ Author: Kevin Barnard ([kbarnard@mbari.org](mailto:kbarnard@mbari.org))
5
+
6
+ ## :hammer: Installation
7
+
8
+ > [!NOTE]
9
+ > VARS Localize requires Python 3.8 or later.
10
+
11
+ To install VARS Localize, run:
12
+ ```bash
13
+ pip install vars-localize
14
+ ```
15
+
16
+ ## :rocket: Usage
17
+
18
+ To start the application, run:
19
+ ```bash
20
+ vars-localize [-u URL]
21
+ ```
22
+
23
+ Once the application launches, log in with your VARS username and password.
24
+
25
+ Search for a concept in the bar at the top left of the application, then select a concept from the list of results to populate a tree of imaged moments in the pane below.
26
+ Select an observation from the children in the subtree of the imaged moment, and draw a bounding box around the observed concept by clicking and dragging.
27
+
28
+ You can double-click on any localization to edit its properties in a dialog.
29
+ Additionally, a localization can be resized by dragging the square corners of its bounding box.
30
+
31
+ ## Credits
32
+
33
+ VARS Localize is made with [PyQt6](https://pypi.org/project/PyQt6/).
34
+
35
+ ---
36
+
37
+ Copyright &copy; 2019 [Monterey Bay Aquarium Research Institute](https://www.mbari.org/)
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "vars-localize"
3
+ version = "0.1.0"
4
+ description = "Tool for creating localization with VARS."
5
+ authors = [
6
+ { name = "Kevin Barnard", email = "kbarnard@mbari.org" }
7
+ ]
8
+ dependencies = [
9
+ "pyqt6>=6.7.1",
10
+ "requests>=2.32.3",
11
+ "qdarkstyle>=3.2.3",
12
+ ]
13
+ readme = "README.md"
14
+ requires-python = ">= 3.8"
15
+
16
+ [project.scripts]
17
+ vars-localize = "vars_localize.__main__:main"
18
+
19
+ [build-system]
20
+ requires = ["hatchling"]
21
+ build-backend = "hatchling.build"
22
+
23
+ [tool.rye]
24
+ managed = true
25
+ dev-dependencies = []
26
+
27
+ [tool.hatch.metadata]
28
+ allow-direct-references = true
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/vars_localize"]
File without changes
@@ -0,0 +1,34 @@
1
+ """
2
+ Main entry point for the VARS Localize application.
3
+ """
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from PyQt6.QtWidgets import QApplication
9
+
10
+ from vars_localize.ui.AppWindow import AppWindow
11
+ from vars_localize.util.endpoints import DEFAULT_M3_URL
12
+
13
+
14
+ def main():
15
+ """
16
+ Main entry point for the VARS Localize application.
17
+ """
18
+ parser = argparse.ArgumentParser(description="VARS Localize")
19
+ parser.add_argument(
20
+ "-u", "--url", type=str, default=DEFAULT_M3_URL, help="URL of M3 server"
21
+ )
22
+ args = parser.parse_args()
23
+
24
+ app = QApplication(sys.argv)
25
+
26
+ window = AppWindow(args.url)
27
+ window.show()
28
+
29
+ exit_code = app.exec()
30
+ sys.exit(exit_code)
31
+
32
+
33
+ if __name__ == "__main__":
34
+ main()
@@ -0,0 +1,28 @@
1
+ """
2
+ Helpers for dealing with assets.
3
+ """
4
+
5
+ from pathlib import Path
6
+ from typing import Union
7
+
8
+
9
+ ASSETS_DIR = Path(__file__).parent.resolve()
10
+
11
+
12
+ def get_asset_path(rel_path: Union[str, Path]) -> Path:
13
+ """
14
+ Get the absolute path to an asset.
15
+
16
+ Args:
17
+ rel_path (Union[str, Path]): The relative path to the asset.
18
+
19
+ Returns:
20
+ Path: The absolute path to the asset.
21
+
22
+ Raises:
23
+ ValueError: If rel_path is an absolute path.
24
+ """
25
+ rel_path = Path(rel_path)
26
+ if rel_path.is_absolute():
27
+ raise ValueError("rel_path must be a relative path.")
28
+ return ASSETS_DIR / rel_path
@@ -0,0 +1,276 @@
1
+ """
2
+ Main application window.
3
+ """
4
+
5
+ from datetime import datetime
6
+
7
+ from PyQt6.QtCore import Qt
8
+ from PyQt6.QtGui import QCloseEvent, QIcon, QAction
9
+ from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QMessageBox, QInputDialog
10
+
11
+ from vars_localize.ui.EntryTree import EntryTreeItem
12
+ from vars_localize.ui.LoginDialog import LoginDialog
13
+ from vars_localize.ui.DisplayPanel import DisplayPanel
14
+ from vars_localize.ui.SearchPanel import SearchPanel
15
+ from vars_localize.util.m3 import (
16
+ check_connection,
17
+ get_all_users,
18
+ get_annotations_by_video_refernce,
19
+ get_imaged_moments_by_image_reference,
20
+ )
21
+ from vars_localize.util.utils import log, split_comma_list
22
+
23
+
24
+ class AppWindow(QMainWindow):
25
+ def __init__(self, m3_url: str, parent=None):
26
+ super(AppWindow, self).__init__(parent)
27
+ self._m3_url = m3_url.rstrip("/")
28
+
29
+ self.setWindowTitle("VARS Localize")
30
+
31
+ log(f"Checking connection to M3 at {self._m3_url}...")
32
+ if not check_connection(self._m3_url):
33
+ log(
34
+ "You are not connected to M3. Check your internet connection and/or VPN.",
35
+ level=2,
36
+ )
37
+ QMessageBox.critical(
38
+ self,
39
+ "No connection to M3",
40
+ "You are not connected to M3. Check your internet connection and/or VPN.",
41
+ )
42
+ exit(1)
43
+ log("Connected.")
44
+
45
+ self.observer = None
46
+ self.observer_role = None
47
+ self.admin_mode = False
48
+
49
+ login_ok = self.login()
50
+ if not login_ok:
51
+ log("You must log in to use this tool.", level=2)
52
+ exit(1)
53
+
54
+ self.central_container = QWidget()
55
+ self.central_container.setLayout(QHBoxLayout())
56
+
57
+ self.search_panel = SearchPanel(parent=self)
58
+ self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.search_panel)
59
+
60
+ self.display_panel = DisplayPanel(parent=self)
61
+ self.central_container.layout().addWidget(self.display_panel)
62
+
63
+ self.setCentralWidget(self.central_container)
64
+
65
+ # Add admin menu if available to user
66
+ if self.observer_role in ("Maint", "Admin"):
67
+ self.add_admin_menu()
68
+
69
+ self.add_search_menu()
70
+ self.add_video_menu()
71
+
72
+ self.display_panel.image_view.observer = self.observer
73
+ self.display_panel.image_view.select_next = self.search_panel.select_next
74
+ self.display_panel.image_view.select_prev = self.search_panel.select_prev
75
+
76
+ self.search_panel.observer = self.observer
77
+
78
+ def load_entry(self, current: EntryTreeItem, previous: EntryTreeItem):
79
+ """
80
+ Load the current entry into the display panel
81
+ :param current: Current selected entry
82
+ :param previous: Previously selected entry
83
+ :return: None
84
+ """
85
+ if current and current.metadata:
86
+ self.display_panel.load_entry(current)
87
+
88
+ def login(self):
89
+ """
90
+ Prompt for observer login
91
+ :return: None
92
+ """
93
+ login_dialog = LoginDialog(parent=self)
94
+ login_dialog._login_form._username_line_edit.setFocus()
95
+ ok = login_dialog.exec()
96
+
97
+ if ok:
98
+ # Get the username/password from the dialog
99
+ username, password = login_dialog.credentials
100
+
101
+ # Set up the M3 configuration, returning False if login fails
102
+ if not self.configure_m3(username, password):
103
+ return False
104
+
105
+ all_valid_users = get_all_users()
106
+ users_dict = {
107
+ user_data["username"]: user_data for user_data in all_valid_users
108
+ }
109
+
110
+ # Set the observer and role
111
+ self.observer = username
112
+ self.observer_role = users_dict[username]["role"]
113
+ else: # Login cancel, return failure
114
+ return False
115
+
116
+ return True # Return success
117
+
118
+ def configure_m3(self, username, password) -> bool:
119
+ """
120
+ Configure endpoints and set up Annosaurus auth
121
+ """
122
+ from vars_localize.util.endpoints import configure as configure_endpoints
123
+ from vars_localize.util.m3 import configure_anno_session
124
+
125
+ try:
126
+ configure_endpoints(self._m3_url, username, password)
127
+ except Exception as e:
128
+ log("Login failed.", level=2)
129
+ log(e, level=2)
130
+ return False
131
+
132
+ configure_anno_session()
133
+
134
+ return True
135
+
136
+ def add_admin_menu(self):
137
+ """
138
+ Add the admin menu for observation modification/deletion
139
+ """
140
+ main_menu = self.menuBar()
141
+ options_menu = main_menu.addMenu("&Options")
142
+
143
+ admin_mode_action = QAction("Admin Mode", options_menu, checkable=True)
144
+
145
+ def set_admin_mode(val):
146
+ if val:
147
+ QMessageBox.warning(
148
+ self,
149
+ "Entering Admin Mode",
150
+ "WARNING: You are now entering administrator mode. This mode allows modification and deletion of observations within VARS.",
151
+ )
152
+ self.admin_mode = val
153
+
154
+ admin_mode_action.toggled.connect(set_admin_mode)
155
+ options_menu.addAction(admin_mode_action)
156
+
157
+ def add_search_menu(self):
158
+ """
159
+ Add the Go menu for non-concept searches
160
+ """
161
+ main_menu = self.menuBar()
162
+ search_menu = main_menu.addMenu("&Search")
163
+
164
+ def search_imaged_moment():
165
+ imaged_moment_uuid_list, ok = QInputDialog.getText(
166
+ self,
167
+ "Imaged Moment UUID Search",
168
+ "Imaged Moment UUID (or comma-separated list)",
169
+ )
170
+ if ok:
171
+ imaged_moment_uuids = split_comma_list(imaged_moment_uuid_list)
172
+ imaged_moment_uuids = list(
173
+ set(imaged_moment_uuids)
174
+ ) # Ensure no duplicates
175
+
176
+ # Set the UUIDs and load the first page
177
+ self.search_panel.set_uuids(imaged_moment_uuids)
178
+ self.search_panel.load_page()
179
+
180
+ search_imaged_moment_action = QAction("Imaged Moment UUID", search_menu)
181
+ search_imaged_moment_action.triggered.connect(search_imaged_moment)
182
+ search_menu.addAction(search_imaged_moment_action)
183
+
184
+ def search_image_reference():
185
+ image_reference_uuid_list, ok = QInputDialog.getText(
186
+ self,
187
+ "Image Reference UUID Search",
188
+ "Image Reference UUID (or comma-separated list)",
189
+ )
190
+ if ok:
191
+ all_image_reference_uuids = split_comma_list(image_reference_uuid_list)
192
+ imaged_moment_uuids = []
193
+ for image_reference_uuid in all_image_reference_uuids:
194
+ res = get_imaged_moments_by_image_reference(image_reference_uuid)
195
+ if res:
196
+ imaged_moment_uuids.extend(
197
+ [item["imaged_moment_uuid"] for item in res]
198
+ )
199
+ imaged_moment_uuids = list(
200
+ set(imaged_moment_uuids)
201
+ ) # Ensure no duplicates
202
+
203
+ # Set the UUIDs and load the first page
204
+ self.search_panel.set_uuids(imaged_moment_uuids)
205
+ self.search_panel.load_page()
206
+
207
+ search_image_reference_action = QAction("Image Reference UUID", search_menu)
208
+ search_image_reference_action.triggered.connect(search_image_reference)
209
+ search_menu.addAction(search_image_reference_action)
210
+
211
+ def search_video_reference():
212
+ video_reference_uuid, ok = QInputDialog.getText(
213
+ self, "Video Reference UUID Search", "Video Reference UUID"
214
+ )
215
+ if ok:
216
+ res = get_annotations_by_video_refernce(video_reference_uuid)
217
+ if res:
218
+ timestamp_uuid_tuples = set()
219
+ for item in res:
220
+ timestamp = datetime.now()
221
+ if "recorded_timestamp" in item:
222
+ try:
223
+ timestamp = datetime.strptime(
224
+ item["recorded_timestamp"], "%Y-%m-%dT%H:%M:%S.%fZ"
225
+ )
226
+ except ValueError:
227
+ timestamp = datetime.strptime(
228
+ item["recorded_timestamp"], "%Y-%m-%dT%H:%M:%SZ"
229
+ )
230
+
231
+ timestamp_uuid_tuples.add(
232
+ (timestamp, item["imaged_moment_uuid"])
233
+ )
234
+
235
+ # Sort by timestamp, then UUID
236
+ timestamp_uuid_tuples = sorted(timestamp_uuid_tuples)
237
+ imaged_moment_uuids = [item[1] for item in timestamp_uuid_tuples]
238
+
239
+ # Set the UUIDs and load the first page
240
+ self.search_panel.set_uuids(imaged_moment_uuids)
241
+ self.search_panel.load_page()
242
+ else:
243
+ # No results, warning dialog
244
+ QMessageBox.warning(
245
+ self,
246
+ "No Results",
247
+ "No results found for video reference UUID: {}".format(
248
+ video_reference_uuid
249
+ ),
250
+ )
251
+
252
+ search_video_reference_action = QAction("Video Reference UUID", search_menu)
253
+ search_video_reference_action.triggered.connect(search_video_reference)
254
+ search_menu.addAction(search_video_reference_action)
255
+
256
+ def add_video_menu(self):
257
+ """
258
+ Add the Video menu for video-level operations
259
+ """
260
+ main_menu = self.menuBar()
261
+ video_menu = main_menu.addMenu("&Video")
262
+
263
+ def open_video():
264
+ self.search_panel.open_video()
265
+
266
+ open_video_action = QAction("Open Video", video_menu)
267
+ open_video_action.triggered.connect(open_video)
268
+ video_menu.addAction(open_video_action)
269
+
270
+ def closeEvent(self, a0: QCloseEvent) -> None:
271
+ """
272
+ Detect window close and tear down components
273
+ :param a0: Close event
274
+ :return: None
275
+ """
276
+ self.deleteLater()