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,287 @@
1
+ import datetime
2
+ import io
3
+ import json
4
+
5
+ import jsonschema
6
+ import numpy as np
7
+ import pandas as pd
8
+ import requests
9
+ from girder.constants import AccessType
10
+ from girder.models.model_base import AccessControlledModel
11
+
12
+ from ..lib.jq import (
13
+ convert_to_jq_notation,
14
+ find_key_paths,
15
+ get_value,
16
+ parse_jq_notation,
17
+ set_value,
18
+ )
19
+
20
+
21
+ class Form(AccessControlledModel):
22
+ def initialize(self):
23
+ self.name = "form"
24
+ self.ensureIndices(["name"])
25
+
26
+ self.exposeFields(
27
+ level=AccessType.READ,
28
+ fields=(
29
+ "_id",
30
+ "name",
31
+ "description",
32
+ "entryFileName",
33
+ "schema",
34
+ "created",
35
+ "updated",
36
+ "gdriveFolderId",
37
+ "folderId",
38
+ "serialize",
39
+ "pathTemplate",
40
+ "uniqueField",
41
+ ),
42
+ )
43
+
44
+ def validate(self, doc):
45
+ return doc
46
+
47
+ def create(
48
+ self,
49
+ name,
50
+ description,
51
+ schema,
52
+ creator,
53
+ folder=None,
54
+ pathTemplate=None,
55
+ entryFileName=None,
56
+ gdriveFolderId=None,
57
+ serialize=False,
58
+ uniqueField=None,
59
+ ):
60
+ now = datetime.datetime.utcnow()
61
+
62
+ form = {
63
+ "name": name,
64
+ "description": description,
65
+ "schema": schema,
66
+ "folderId": None,
67
+ "gdriveFolderId": gdriveFolderId,
68
+ "pathTemplate": pathTemplate,
69
+ "entryFileName": entryFileName or "entry.json",
70
+ "serialize": serialize,
71
+ "created": now,
72
+ "updated": now,
73
+ "uniqueField": uniqueField or "sampleId",
74
+ }
75
+ if folder:
76
+ form["folderId"] = folder["_id"]
77
+
78
+ return self.save(form)
79
+
80
+ def update(
81
+ self,
82
+ form,
83
+ name,
84
+ description,
85
+ schema,
86
+ folder=None,
87
+ pathTemplate=None,
88
+ entryFileName=None,
89
+ gdriveFolderId=None,
90
+ serialize=None,
91
+ uniqueField=None,
92
+ ):
93
+ now = datetime.datetime.utcnow()
94
+
95
+ form["name"] = name
96
+ form["description"] = description
97
+ form["schema"] = schema
98
+ form["updated"] = now
99
+ form["pathTemplate"] = pathTemplate
100
+
101
+ if folder:
102
+ form["folderId"] = folder["_id"]
103
+ else:
104
+ form["folderId"] = None
105
+
106
+ if entryFileName:
107
+ form["entryFileName"] = entryFileName
108
+
109
+ if gdriveFolderId:
110
+ form["gdriveFolderId"] = gdriveFolderId
111
+
112
+ if serialize is not None:
113
+ form["serialize"] = serialize
114
+
115
+ if uniqueField:
116
+ form["uniqueField"] = uniqueField
117
+
118
+ return self.save(form)
119
+
120
+ def materialize(self, form, user):
121
+ from .entry import FormEntry
122
+
123
+ if form["schema"].startswith("http"):
124
+ form["schema"] = self._loadRemoteSchema(form["schema"])
125
+ else:
126
+ form["schema"] = json.loads(form["schema"])
127
+
128
+ for keyPath in find_key_paths(form["schema"], "enumSource"):
129
+ value = get_value(form["schema"], keyPath)
130
+ if isinstance(value, str) and value.startswith("girder.formId:"):
131
+ formId = value.split(":")[1]
132
+ source_form = self.load(
133
+ formId, level=AccessType.READ, user=user, exc=True
134
+ )
135
+ enum_source = {
136
+ "source": [],
137
+ "title": "{{item.title}}",
138
+ "value": "{{item.value}}",
139
+ }
140
+ for entry in (
141
+ FormEntry()
142
+ .find(
143
+ {"formId": source_form["_id"]},
144
+ fields={"_id": 1, source_form["uniqueField"]: 1, "data": 1},
145
+ )
146
+ .sort([(source_form["uniqueField"], 1)])
147
+ ):
148
+ enum_source["source"].append(
149
+ {
150
+ "value": str(entry["_id"]),
151
+ "title": entry["data"][source_form["uniqueField"]],
152
+ }
153
+ )
154
+ set_value(form["schema"], keyPath, [enum_source])
155
+
156
+ return form
157
+
158
+ def export(self, form, export_format):
159
+ from .entry import FormEntry
160
+
161
+ entries = [
162
+ convert_to_jq_notation(entry)
163
+ for entry in FormEntry()
164
+ .find(
165
+ {"formId": form["_id"]},
166
+ fields={"_id": 1, "data": 1},
167
+ )
168
+ .sort([("created", 1)])
169
+ ]
170
+ buffer = io.BytesIO()
171
+ if export_format == "csv":
172
+ pd.DataFrame(entries).to_csv(buffer, index=False)
173
+ elif export_format == "xlsx":
174
+ pd.DataFrame(entries).to_excel(buffer, index=False)
175
+ return buffer
176
+
177
+ @staticmethod
178
+ def resolve_ref(ref, definitions):
179
+ ref_path = ref.lstrip("#/").split("/")
180
+ ref_value = definitions
181
+ for part in ref_path[1:]:
182
+ ref_value = ref_value.get(part, {})
183
+ return ref_value
184
+
185
+ def get_data_types(self, form):
186
+ schema = json.loads(form["schema"])
187
+ types = self.infer_column_types(
188
+ schema, definitions=schema.get("definitions", {})
189
+ )
190
+ return {"data." + key: value for key, value in types.items()}
191
+
192
+ def schema_type_to_pandas(self, typename):
193
+ if isinstance(typename, list):
194
+ for t in typename:
195
+ if t == "null":
196
+ continue
197
+ return self.schema_type_to_pandas(t)
198
+ if typename == "string":
199
+ return "object"
200
+ if typename == "integer":
201
+ return "int64"
202
+ if typename == "number":
203
+ return "float64"
204
+ if typename == "boolean":
205
+ return "bool"
206
+
207
+ def infer_column_types(self, schema, definitions=None, parent_key=""):
208
+ properties = {}
209
+ definitions = definitions or {}
210
+
211
+ if "properties" in schema:
212
+ for key, value in schema["properties"].items():
213
+ full_key = f"{parent_key}.{key}" if parent_key else key
214
+
215
+ if "$ref" in value:
216
+ value = self.resolve_ref(value["$ref"], definitions)
217
+
218
+ if (
219
+ "type" in value
220
+ and value["type"] != "object"
221
+ and value["type"] != "array"
222
+ ):
223
+ properties[full_key] = self.schema_type_to_pandas(value["type"])
224
+
225
+ if value.get("type", "object") == "object":
226
+ properties.update(
227
+ self.infer_column_types(
228
+ value, definitions=definitions or None, parent_key=full_key
229
+ )
230
+ )
231
+
232
+ if value.get("type") == "array" and "items" in value:
233
+ properties[full_key] = "array"
234
+ properties.update(
235
+ self.infer_column_types(
236
+ value["items"],
237
+ definitions=definitions or None,
238
+ parent_key=full_key + "[]",
239
+ )
240
+ )
241
+
242
+ return properties
243
+
244
+ def import_entries(self, form, file_obj, file_type, dry_run=True):
245
+ from .entry import FormEntry
246
+
247
+ schema = json.loads(form["schema"])
248
+ io_buffer = io.BytesIO(file_obj.read())
249
+ if file_type == "csv":
250
+ entries = pd.read_csv(
251
+ io_buffer, na_values=["None", "nan"], dtype=self.get_data_types(form)
252
+ )
253
+ elif file_type == "xlsx":
254
+ entries = pd.read_excel(io_buffer)
255
+
256
+ for col in entries.columns:
257
+ if entries[col].dtype == "object":
258
+ entries[col] = entries[col].replace(np.nan, "")
259
+
260
+ parsed_entries = [
261
+ parse_jq_notation(entry) for entry in entries.to_dict(orient="records")
262
+ ]
263
+ failed = new = updated = 0
264
+ for row in parsed_entries:
265
+ entry = row["data"]
266
+ try:
267
+ jsonschema.Draft7Validator(schema).validate(entry)
268
+ except jsonschema.ValidationError as e:
269
+ print(str(e))
270
+ failed += 1
271
+ continue
272
+ try:
273
+ unique_id = entry[form["uniqueField"]]
274
+ except KeyError:
275
+ failed += 1
276
+ continue
277
+ if existing_entry := FormEntry().findOne(
278
+ {"formId": form["_id"], f"data.{form['uniqueField']}": unique_id}
279
+ ):
280
+ updated += 1
281
+ print("Updating entry", existing_entry["_id"])
282
+ else:
283
+ new += 1
284
+ return json.dumps({"new": new, "updated": updated, "failed": failed})
285
+
286
+ def _loadRemoteSchema(self, url):
287
+ return requests.get(url).json()
File without changes
@@ -0,0 +1,126 @@
1
+ from girder.api import access
2
+ from girder.api.describe import Description, autoDescribeRoute
3
+ from girder.api.rest import Resource, filtermodel
4
+ from girder.constants import AccessType, TokenScope
5
+ from girder.models.folder import Folder
6
+
7
+ from ..models.form import Form as FormModel
8
+ from ..models.entry import FormEntry as FormEntryModel
9
+
10
+
11
+ class FormEntry(Resource):
12
+ def __init__(self):
13
+ super(FormEntry, self).__init__()
14
+ self.resourceName = "entry"
15
+ self.route("GET", (), self.listFormEntry)
16
+ self.route("GET", ("search",), self.searchFormEntry)
17
+ self.route("GET", (":id",), self.getFormEntry)
18
+ self.route("POST", (), self.createFormEntry)
19
+ self.route("DELETE", (":id",), self.deleteFormEntry)
20
+
21
+ @access.public
22
+ @autoDescribeRoute(
23
+ Description("List all entries")
24
+ .modelParam(
25
+ "formId",
26
+ "The ID of the form",
27
+ model=FormModel,
28
+ level=AccessType.READ,
29
+ required=False,
30
+ )
31
+ .pagingParams(defaultSort="created")
32
+ )
33
+ def listFormEntry(self, form, limit, offset, sort):
34
+ q = {}
35
+ if form:
36
+ q = {"formId": form["_id"]}
37
+
38
+ cursor = FormEntryModel().findWithPermissions(
39
+ q,
40
+ sort=sort,
41
+ user=self.getCurrentUser(),
42
+ level=AccessType.READ,
43
+ limit=limit,
44
+ offset=offset,
45
+ )
46
+ return list(cursor)
47
+
48
+ @access.public
49
+ @autoDescribeRoute(
50
+ Description("Search entries")
51
+ .param("query", "Regex for Sample Id", dataType="string", required=True)
52
+ .pagingParams(defaultSort="data.sampleId")
53
+ )
54
+ def searchFormEntry(self, query, limit, offset, sort):
55
+ print(query)
56
+ q = {"data.sampleId": {"$regex": query}}
57
+ cursor = FormEntryModel().findWithPermissions(
58
+ q,
59
+ user=self.getCurrentUser(),
60
+ level=AccessType.READ,
61
+ limit=limit,
62
+ offset=offset,
63
+ sort=sort,
64
+ )
65
+ return list(cursor)
66
+
67
+ @access.public
68
+ @autoDescribeRoute(
69
+ Description("Get an entry by ID").modelParam(
70
+ "id", "The ID of the form", model=FormEntryModel, level=AccessType.READ
71
+ )
72
+ )
73
+ @filtermodel(model=FormEntryModel, plugin="jsonforms")
74
+ def getFormEntry(self, entry):
75
+ return entry
76
+
77
+ @access.user(scope=TokenScope.DATA_WRITE)
78
+ @autoDescribeRoute(
79
+ Description("Create a new entry")
80
+ .modelParam(
81
+ "formId",
82
+ "The ID of the form",
83
+ model=FormModel,
84
+ level=AccessType.READ,
85
+ destName="form",
86
+ paramType="query",
87
+ required=True,
88
+ )
89
+ .jsonParam("data", "The data of the entry", required=True)
90
+ .modelParam(
91
+ "sourceId",
92
+ "The folder ID of uploaded data",
93
+ required=False,
94
+ model=Folder,
95
+ paramType="query",
96
+ destName="source",
97
+ level=AccessType.WRITE,
98
+ )
99
+ .modelParam(
100
+ "destinationId",
101
+ "The folder ID of destination",
102
+ required=True,
103
+ model=Folder,
104
+ paramType="query",
105
+ destName="destination",
106
+ level=AccessType.WRITE,
107
+ )
108
+ )
109
+ @filtermodel(model=FormEntryModel, plugin="jsonforms")
110
+ def createFormEntry(self, form, data, source, destination):
111
+ return FormEntryModel().create(
112
+ form,
113
+ data,
114
+ source,
115
+ destination,
116
+ self.getCurrentUser(),
117
+ )
118
+
119
+ @access.user(scope=TokenScope.DATA_WRITE)
120
+ @autoDescribeRoute(
121
+ Description("Delete an entry").modelParam(
122
+ "id", "The ID of the entry", model=FormEntryModel, level=AccessType.WRITE
123
+ )
124
+ )
125
+ def deleteFormEntry(self, entry):
126
+ FormEntryModel().remove(entry)