girder-jsonforms 2.0.0__py3-none-any.whl

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.
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ import json
4
+ import logging
5
+ import os
6
+
7
+ from girder import events
8
+ from girder.constants import AccessType
9
+ from girder.models.file import File
10
+ from girder.models.item import Item
11
+ from girder.plugin import GirderPlugin, registerPluginStaticContent
12
+ from girder.utility.model_importer import ModelImporter
13
+
14
+ from .lib.google_drive import authenticate_gdrive, upload_file_to_gdrive
15
+ from .models.entry import FormEntry as FormEntryModel
16
+ from .models.form import Form as FormModel
17
+ from .rest.entry import FormEntry
18
+ from .rest.form import Form
19
+
20
+ GDRIVE_SERVICE = None
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def annotate_uploads(event):
25
+ info = event.info
26
+ if "reference" not in info:
27
+ return
28
+
29
+ try:
30
+ reference = json.loads(info["reference"])
31
+ except (ValueError, TypeError):
32
+ return
33
+
34
+ if reference.get("annotate"):
35
+ parent = Item().load(
36
+ info["file"]["itemId"], level=AccessType.WRITE, user=info["currentUser"]
37
+ )
38
+ reference.pop("file", None)
39
+ reference.pop("annotate", None)
40
+ Item().setMetadata(parent, reference)
41
+
42
+
43
+ def upload_to_gdrive(event):
44
+ global GDRIVE_SERVICE
45
+ info = event.info
46
+ file = info["file"]
47
+
48
+ with File().open(file) as fh:
49
+ gdrive_file_id = upload_file_to_gdrive(
50
+ GDRIVE_SERVICE,
51
+ info["gdriveFolderId"],
52
+ info["path"],
53
+ fh,
54
+ mimetype=file["mimeType"],
55
+ )
56
+ parent = Item().load(
57
+ file["itemId"], level=AccessType.WRITE, user=info["currentUser"]
58
+ )
59
+ Item().setMetadata(parent, {"gdriveFileId": gdrive_file_id})
60
+
61
+
62
+ class JSONFormsPlugin(GirderPlugin):
63
+ DISPLAY_NAME = "JSON Forms"
64
+
65
+ def load(self, info):
66
+ ModelImporter.registerModel("form", FormModel, plugin="jsonforms")
67
+ ModelImporter.registerModel("entry", FormEntryModel, plugin="jsonforms")
68
+ global GDRIVE_SERVICE
69
+ try:
70
+ GDRIVE_SERVICE = authenticate_gdrive()
71
+ except ValueError:
72
+ logger.exception("Failed to authenticate with Google Drive")
73
+ info["apiRoot"].form = Form()
74
+ info["apiRoot"].entry = FormEntry()
75
+ events.bind("data.process", "jsonforms", annotate_uploads)
76
+ events.bind("gdrive.upload", "jsonforms", upload_to_gdrive)
77
+ registerPluginStaticContent(
78
+ plugin="jsonforms",
79
+ css=["/style.css"],
80
+ js=["/girder-plugin-jsonforms.umd.cjs"],
81
+ staticDir=os.path.join(os.path.dirname(__file__), "web_client", "dist"),
82
+ tree=info["serverRoot"],
83
+ )
File without changes
@@ -0,0 +1,157 @@
1
+ import os
2
+ import pickle
3
+ import logging
4
+
5
+ from google.auth.transport.requests import Request
6
+
7
+ # from google_auth_oauthlib.flow import InstalledAppFlow
8
+ from googleapiclient.discovery import build
9
+ from googleapiclient.http import MediaIoBaseUpload
10
+
11
+ logger = logging.getLogger("google_drive")
12
+ logprint = logger
13
+ # If modifying these scopes, delete the file token.pickle.
14
+ SCOPES = ["https://www.googleapis.com/auth/drive"]
15
+
16
+
17
+ def get_credentials():
18
+ """Gets valid user credentials from storage.
19
+
20
+ If nothing has been stored, or if the stored credentials are invalid,
21
+ the OAuth2 flow is completed to obtain the new credentials.
22
+
23
+ Returns:
24
+ Credentials, the obtained credential.
25
+ """
26
+ creds = None
27
+ # The file token.pickle stores the user's access and refresh tokens, and is
28
+ # created automatically when the authorization flow completes for the first
29
+ # time.
30
+
31
+ path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "token.pickle")
32
+
33
+ if os.path.exists(path):
34
+ with open(path, "rb") as token:
35
+ creds = pickle.load(token)
36
+ # If there are no (valid) credentials available, let the user log in.
37
+ if not creds or not creds.valid:
38
+ if creds and creds.expired and creds.refresh_token:
39
+ creds.refresh(Request())
40
+ else:
41
+ raise ValueError("No valid credentials found")
42
+ # flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
43
+ # creds = flow.run_local_server(port=0)
44
+ # Save the credentials for the next run
45
+ with open(path, "wb") as token:
46
+ pickle.dump(creds, token)
47
+ return creds
48
+
49
+
50
+ def authenticate_gdrive():
51
+ # Load credentials from a file or any other secure source
52
+ # Replace 'path_to_credentials.json' with the actual path to your credentials file
53
+ credentials = get_credentials()
54
+ return build("drive", "v3", credentials=credentials)
55
+
56
+
57
+ def create_folders(service, folder_id, path):
58
+ if service is None:
59
+ logger.exception("Service not initialized. Failed to create folder.")
60
+ return
61
+ current_folder_id = folder_id
62
+ folders = path.split("/")
63
+ for folder in folders:
64
+ if folder:
65
+ # Check if folder already exists
66
+ query = f"'{current_folder_id}' in parents and mimeType='application/vnd.google-apps.folder' and name='{folder}' and trashed=false"
67
+ response = (
68
+ service.files()
69
+ .list(
70
+ q=query,
71
+ fields="files(id)",
72
+ includeItemsFromAllDrives=True,
73
+ supportsAllDrives=True,
74
+ )
75
+ .execute()
76
+ )
77
+ existing_folders = response.get("files", [])
78
+ if existing_folders:
79
+ logprint.error(f"Folder '{folder}' already exists")
80
+ logprint.error(response)
81
+ current_folder_id = existing_folders[0]["id"]
82
+ else:
83
+ logprint.error(f"Creating folder '{folder}'")
84
+ file_metadata = {
85
+ "name": folder,
86
+ "mimeType": "application/vnd.google-apps.folder",
87
+ "parents": [current_folder_id],
88
+ }
89
+ folder_result = (
90
+ service.files()
91
+ .create(body=file_metadata, fields="id", supportsAllDrives=True)
92
+ .execute()
93
+ )
94
+ logprint.error(
95
+ f"Folder '{folder}' created with ID: {folder_result['id']}"
96
+ )
97
+ current_folder_id = folder_result["id"]
98
+
99
+ return current_folder_id
100
+
101
+
102
+ def upload_file_to_gdrive(service, folder_id, path, file_handle, mimetype="text/plain"):
103
+ if service is None:
104
+ logger.exception("Service not initialized. Failed to upload file.")
105
+ return
106
+ file_path, file_name = os.path.split(path)
107
+ folder_id = create_folders(service, folder_id, file_path)
108
+ logprint.error(f"Uploading file '{file_name}' to folder '{folder_id}'")
109
+
110
+ # Check if file already exists in the folder
111
+ query = f"'{folder_id}' in parents and name='{file_name}' and trashed=false"
112
+ response = (
113
+ service.files()
114
+ .list(
115
+ q=query,
116
+ fields="files(id)",
117
+ includeItemsFromAllDrives=True,
118
+ supportsAllDrives=True,
119
+ )
120
+ .execute()
121
+ )
122
+ existing_files = response.get("files", [])
123
+ media = MediaIoBaseUpload(file_handle, mimetype=mimetype)
124
+ if existing_files:
125
+ file_id = existing_files[0]["id"]
126
+ file_result = (
127
+ service.files()
128
+ .update(fileId=file_id, media_body=media, supportsAllDrives=True)
129
+ .execute()
130
+ )
131
+ logprint.error(f'File {file_name} updated with ID: {file_result["id"]}')
132
+ else:
133
+ file_metadata = {"name": file_name, "parents": [folder_id]}
134
+ file_result = (
135
+ service.files()
136
+ .create(
137
+ body=file_metadata,
138
+ media_body=media,
139
+ fields="id",
140
+ supportsAllDrives=True,
141
+ )
142
+ .execute()
143
+ )
144
+ logprint.error(
145
+ f'File {file_metadata["name"]} uploaded with ID: {file_result["id"]}'
146
+ )
147
+ return file_result["id"]
148
+
149
+
150
+ # Example usage
151
+ if __name__ == "__main__":
152
+ # folder_id = "1WgY3HJ0bxzQc-IA_dBdiwb9Hvq6ho3HS" # Replace with the actual folder ID
153
+ folder_id = "13u4Citrr09FETrPDn2gJmBHWeaO0ylji"
154
+ path = "subfolder/subfolder2/subfolder4/file.txt"
155
+ file_handle = open("file.txt", "rb") # Replace 'file.txt' with the actual file path
156
+ service = authenticate_gdrive()
157
+ file_id = upload_file_to_gdrive(service, folder_id, path, file_handle)
@@ -0,0 +1,134 @@
1
+ from typing import Any
2
+
3
+ # find all occurences of a key in a nested json
4
+ def find_key_paths(json_data: dict, key: str, path: str = "") -> list[str]:
5
+ results = []
6
+ # Check if the input is a dictionary
7
+ if isinstance(json_data, dict):
8
+ # If the key is found, return the path to it
9
+ if key in json_data:
10
+ results.append(path + key)
11
+ # Recursively search through each value in the dictionary
12
+ for k, v in json_data.items():
13
+ results.extend(find_key_paths(v, key, path + k + "."))
14
+ # Check if the input is a list
15
+ elif isinstance(json_data, list):
16
+ # Recursively search through each item in the list
17
+ for i, item in enumerate(json_data):
18
+ if isinstance(item, dict):
19
+ results.extend(find_key_paths(item, key, path + f"[{i}]."))
20
+ elif isinstance(item, list):
21
+ for j, sub_item in enumerate(item):
22
+ results.extend(find_key_paths(sub_item, key, path + f"[{i}][{j}]."))
23
+ # Return None if the key is not found
24
+ return results
25
+
26
+
27
+ # method to change the value of the key using key notation .[]
28
+ def get_value(json_data: dict, key: str) -> dict:
29
+ for path in key.split("."):
30
+ if path.startswith("["):
31
+ index = int(path[1:-1])
32
+ json_data = json_data[index]
33
+ else:
34
+ json_data = json_data[path]
35
+ return json_data
36
+
37
+
38
+ def set_value(json_data: dict, key: str, value: Any) -> None:
39
+ for path in key.split(".")[:-1]:
40
+ if path.startswith("["):
41
+ index = int(path[1:-1])
42
+ json_data = json_data[index]
43
+ else:
44
+ json_data = json_data[path]
45
+ path = key.split(".")[-1]
46
+ json_data[path] = value
47
+
48
+
49
+ def convert_to_jq_notation(data: dict, parent_key: str = "", sep: str = ".") -> dict:
50
+ """
51
+ Convert a nested dictionary or list into a flat dictionary with jq-style keys.
52
+
53
+ Args:
54
+ data (dict or list): The nested dictionary or list to flatten.
55
+ parent_key (str): The base key for recursion (used internally).
56
+ sep (str): The separator to use for keys (default is ".").
57
+
58
+ Returns:
59
+ dict: A flattened dictionary with jq-like notation keys.
60
+ """
61
+ items = []
62
+
63
+ if isinstance(data, dict):
64
+ for key, value in data.items():
65
+ new_key = f"{parent_key}{sep}{key}" if parent_key else key
66
+ items.extend(convert_to_jq_notation(value, new_key, sep).items())
67
+ elif isinstance(data, list):
68
+ for index, value in enumerate(data):
69
+ new_key = f"{parent_key}{sep}[{index}]"
70
+ items.extend(convert_to_jq_notation(value, new_key, sep).items())
71
+ else:
72
+ items.append((parent_key, data))
73
+
74
+ return dict(items)
75
+
76
+
77
+ def parse_jq_notation(input_dict: dict) -> dict:
78
+ """
79
+ Convert a dictionary with jq-style keys into a nested dictionary.
80
+
81
+ Args:
82
+ input_dict (dict): The dictionary with jq-style keys.
83
+
84
+ Returns:
85
+ dict: A nested dictionary.
86
+ """
87
+
88
+ def is_array_index(key):
89
+ return key.startswith("[") and key.endswith("]") and key[1:-1].isdigit()
90
+
91
+ output = {}
92
+
93
+ for key, value in input_dict.items():
94
+ keys = key.split(".")
95
+ current = output
96
+
97
+ for i, part in enumerate(keys[:-1]):
98
+ if is_array_index(part):
99
+ # Convert string digits to integers for lists
100
+ part = int(part.strip("[]"))
101
+
102
+ if "nestedlist" in key:
103
+ print(current, key, part)
104
+ if isinstance(part, int):
105
+ # If current is not a list, convert it into one
106
+ if not isinstance(current, list):
107
+ current[keys[i - 1]] = []
108
+ current = current[keys[i - 1]]
109
+ # Extend the list to ensure the index exists
110
+
111
+ if i + 1 <= len(keys[:-1]) and is_array_index(keys[i+1]):
112
+ append_object = []
113
+ else:
114
+ append_object = {}
115
+
116
+ while len(current) <= part:
117
+ # It has to be a placeholder for dictionary or list
118
+ current.append(append_object)
119
+ current = current[part]
120
+ else:
121
+ if part not in current:
122
+ # Create a list if the next key is a digit
123
+ next_part = keys[i + 1]
124
+ current[part] = [] if is_array_index(next_part) else {}
125
+ current = current[part]
126
+
127
+ # Handle the last key
128
+ last_key = keys[-1]
129
+ if is_array_index(last_key):
130
+ current.append(value)
131
+ else:
132
+ current[last_key] = value
133
+
134
+ return output
File without changes
@@ -0,0 +1,245 @@
1
+ import datetime
2
+ import io
3
+ import json
4
+ import os
5
+
6
+ from girder import events
7
+ from girder.constants import AccessType
8
+ from girder.models.folder import Folder
9
+ from girder.models.item import Item
10
+ from girder.models.model_base import Model
11
+ from girder.models.upload import Upload
12
+ from girder.utility import acl_mixin, JsonEncoder, RequestBodyStream
13
+
14
+
15
+ class FormEntry(acl_mixin.AccessControlMixin, Model):
16
+ def initialize(self):
17
+ global GDRIVE_SERVICE
18
+ self.name = "entry"
19
+ # TODO: create indices for all pairs?
20
+ # self.ensureIndices(["formId", "data.sampleId"])
21
+ self.resourceColl = ("form", "jsonforms")
22
+ self.resourceParent = "formId"
23
+
24
+ self.exposeFields(
25
+ level=AccessType.READ,
26
+ fields=(
27
+ "_id",
28
+ "formId",
29
+ "folderId",
30
+ "data",
31
+ "created",
32
+ "updated",
33
+ "files",
34
+ "folders",
35
+ ),
36
+ )
37
+
38
+ def validate(self, doc):
39
+ return doc
40
+
41
+ def _getExtraPath(self, template, data):
42
+ # Define a safe set of built-in functions and variables
43
+ safe_globals = {"__builtins__": None}
44
+ safe_locals = {"data": data, "ord": ord}
45
+
46
+ # Evaluate the template
47
+ try:
48
+ result = eval(f'f"{template}"', safe_globals, safe_locals)
49
+ return result
50
+ except Exception as e:
51
+ print("Error:", e)
52
+ return None
53
+
54
+ def create(self, form, data, source, destination, creator):
55
+ now = datetime.datetime.utcnow()
56
+ unique_field = form.get("uniqueField")
57
+ entry = {
58
+ "formId": form["_id"],
59
+ "data": data,
60
+ "created": now,
61
+ "updated": now,
62
+ "folderId": destination["_id"],
63
+ "files": [],
64
+ "folders": [],
65
+ }
66
+
67
+ if existing := self.findOne(
68
+ {
69
+ "formId": form["_id"],
70
+ f"data.{unique_field}": data[unique_field],
71
+ }
72
+ ):
73
+ # Update the existing entry
74
+ entry.update(
75
+ {
76
+ "_id": existing["_id"],
77
+ "created": existing["created"],
78
+ "files": existing["files"],
79
+ "folders": existing["folders"],
80
+ }
81
+ )
82
+
83
+ # Move from temp to destination
84
+ path = entry["data"].get("targetPath")
85
+ known_targets = {
86
+ None: (
87
+ self.get_destination_folder(path, destination, creator),
88
+ entry["data"].get(unique_field),
89
+ )
90
+ }
91
+ if source is not None:
92
+ for child in Folder().childFolders(source, "folder", user=creator):
93
+ path = child.get("meta", {}).get("targetPath")
94
+ try:
95
+ target, _ = known_targets[path]
96
+ except KeyError:
97
+ target = self.get_destination_folder(path, destination, creator)
98
+ known_targets[path] = target, child.get("meta", {}).get(unique_field)
99
+ child = self.unique(child, target)
100
+ Folder().move(child, target, "folder")
101
+ # TODO upload to GDrive
102
+ entry["folders"].append(child["_id"])
103
+
104
+ for child in Folder().childItems(source):
105
+ path = child.get("meta", {}).get("targetPath")
106
+ try:
107
+ target, _ = known_targets[path]
108
+ except KeyError:
109
+ target = self.get_destination_folder(path, destination, creator)
110
+ known_targets[path] = target, child.get("meta", {}).get(unique_field)
111
+ child = self.unique(child, target)
112
+ child = Item().move(child, target)
113
+ for file in Item().childFiles(child):
114
+ # Upload to GDrive
115
+ gdrive_folder_id = child.get("meta", {}).get("gdriveFolderId")
116
+ if gdrive_folder_id:
117
+ events.daemon.trigger(
118
+ "gdrive.upload",
119
+ {
120
+ "file": file,
121
+ "gdriveFolderId": gdrive_folder_id,
122
+ "path": os.path.join(path, file["name"]),
123
+ "currentUser": creator,
124
+ },
125
+ )
126
+ entry["files"].append(child["_id"])
127
+ Folder().remove(source)
128
+
129
+ if not form.get("serialize", False):
130
+ return self.save(entry)
131
+
132
+ # Dump the entry into json file, by creating bytes buffer from json dump and
133
+ # Upload().uploadFromFile will create a file in each destination folder
134
+ if len(known_targets) > 1:
135
+ known_targets.pop(None)
136
+
137
+ processed = set()
138
+ for path, (target, uniqueId) in known_targets.items():
139
+ if target["_id"] in processed:
140
+ continue
141
+ path = path or entry["data"].get("targetPath")
142
+ with io.BytesIO(
143
+ json.dumps(
144
+ entry, sort_keys=True, allow_nan=False, cls=JsonEncoder
145
+ ).encode("utf-8")
146
+ ) as f:
147
+ reference = {
148
+ f"{unique_field}": uniqueId,
149
+ "targetPath": path,
150
+ "gdriveFolderId": form.get("gdriveFolderId"),
151
+ }
152
+ size = f.getbuffer().nbytes
153
+ upload = self._get_upload_for_entry(
154
+ form["entryFileName"], target, creator, size, reference
155
+ )
156
+ # not really chunking here as JSON is small
157
+ upload = Upload().handleChunk(upload, RequestBodyStream(f, size))
158
+ if form.get("gdriveFolderId"):
159
+ events.daemon.trigger(
160
+ "gdrive.upload",
161
+ {
162
+ "file": upload,
163
+ "gdriveFolderId": form["gdriveFolderId"],
164
+ "gdriveFileId": reference.get("gdriveFileId"),
165
+ "path": os.path.join(path, upload["name"]),
166
+ "currentUser": creator,
167
+ },
168
+ )
169
+ processed.add(target["_id"])
170
+
171
+ return self.save(entry)
172
+
173
+ @staticmethod
174
+ def _get_upload_for_entry(fname, target, creator, size, reference):
175
+ if existing_item := Item().findOne({"name": fname, "folderId": target["_id"]}):
176
+ file = Item().childFiles(existing_item)[0]
177
+ if "gdriveFileId" in existing_item.get("meta", {}):
178
+ reference["gdriveFileId"] = existing_item["meta"]["gdriveFileId"]
179
+ reference["itemId"] = existing_item["_id"]
180
+ serialized_reference = json.dumps(
181
+ reference, sort_keys=True, allow_nan=False, cls=JsonEncoder
182
+ )
183
+ upload = Upload().createUploadToFile(
184
+ file=file, user=creator, size=size, reference=serialized_reference
185
+ )
186
+ else:
187
+ serialized_reference = json.dumps(
188
+ reference, sort_keys=True, allow_nan=False, cls=JsonEncoder
189
+ )
190
+ upload = Upload().createUpload(
191
+ user=creator,
192
+ name=fname,
193
+ parentType="folder",
194
+ parent=target,
195
+ size=size,
196
+ mimeType="application/json",
197
+ reference=serialized_reference,
198
+ )
199
+ return upload
200
+
201
+ @staticmethod
202
+ def get_destination_folder(path, root, user):
203
+ if path is None:
204
+ return root
205
+
206
+ destination = root
207
+ for subfolder in path.split(os.path.sep):
208
+ destination = Folder().createFolder(
209
+ destination,
210
+ subfolder,
211
+ parentType="folder",
212
+ creator=user,
213
+ reuseExisting=True,
214
+ )
215
+
216
+ return destination
217
+
218
+ @staticmethod
219
+ def unique(child, destination):
220
+ name = child["name"]
221
+ n = 0
222
+ checkName = True
223
+ while checkName:
224
+ q = {
225
+ "name": name,
226
+ "folderId": destination["_id"],
227
+ "_id": {"$ne": child["_id"]},
228
+ }
229
+ dupItem = Item().findOne(q, fields=["_id"])
230
+ q = {
231
+ "name": name,
232
+ "parentId": destination["_id"],
233
+ "parentCollection": "folder",
234
+ }
235
+ dupFolder = Folder().findOne(q, fields=["_id"])
236
+
237
+ if dupItem is None and dupFolder is None:
238
+ child["name"] = name
239
+ checkName = False
240
+ else:
241
+ n += 1
242
+ name = f"{child['name']} ({n})"
243
+
244
+ child["lowerName"] = child["name"].lower()
245
+ return child