labkey 2.6.0__tar.gz → 3.0.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 (29) hide show
  1. {labkey-2.6.0 → labkey-3.0.0}/CHANGE.txt +17 -0
  2. {labkey-2.6.0/labkey.egg-info → labkey-3.0.0}/PKG-INFO +9 -3
  3. {labkey-2.6.0 → labkey-3.0.0}/README.md +1 -0
  4. {labkey-2.6.0 → labkey-3.0.0}/labkey/__init__.py +1 -1
  5. {labkey-2.6.0 → labkey-3.0.0}/labkey/query.py +180 -7
  6. {labkey-2.6.0 → labkey-3.0.0}/labkey/utils.py +20 -0
  7. {labkey-2.6.0 → labkey-3.0.0/labkey.egg-info}/PKG-INFO +9 -3
  8. {labkey-2.6.0 → labkey-3.0.0}/labkey.egg-info/SOURCES.txt +0 -4
  9. labkey-2.6.0/download_file_example.txt +0 -69
  10. labkey-2.6.0/lundbeck_file_download_notes.txt +0 -10
  11. labkey-2.6.0/perf_testing_notes.txt +0 -20
  12. labkey-2.6.0/playground.py.txt +0 -46
  13. {labkey-2.6.0 → labkey-3.0.0}/LICENSE.txt +0 -0
  14. {labkey-2.6.0 → labkey-3.0.0}/MANIFEST.in +0 -0
  15. {labkey-2.6.0 → labkey-3.0.0}/labkey/api_wrapper.py +0 -0
  16. {labkey-2.6.0 → labkey-3.0.0}/labkey/container.py +0 -0
  17. {labkey-2.6.0 → labkey-3.0.0}/labkey/domain.py +0 -0
  18. {labkey-2.6.0 → labkey-3.0.0}/labkey/exceptions.py +0 -0
  19. {labkey-2.6.0 → labkey-3.0.0}/labkey/experiment.py +0 -0
  20. {labkey-2.6.0 → labkey-3.0.0}/labkey/security.py +0 -0
  21. {labkey-2.6.0 → labkey-3.0.0}/labkey/server_context.py +0 -0
  22. {labkey-2.6.0 → labkey-3.0.0}/labkey/storage.py +0 -0
  23. {labkey-2.6.0 → labkey-3.0.0}/labkey.egg-info/dependency_links.txt +0 -0
  24. {labkey-2.6.0 → labkey-3.0.0}/labkey.egg-info/requires.txt +0 -0
  25. {labkey-2.6.0 → labkey-3.0.0}/labkey.egg-info/top_level.txt +0 -0
  26. {labkey-2.6.0 → labkey-3.0.0}/pyproject.toml +0 -0
  27. {labkey-2.6.0 → labkey-3.0.0}/pytest.ini +0 -0
  28. {labkey-2.6.0 → labkey-3.0.0}/setup.cfg +0 -0
  29. {labkey-2.6.0 → labkey-3.0.0}/setup.py +0 -0
@@ -2,6 +2,23 @@
2
2
  LabKey Python Client API News
3
3
  +++++++++++
4
4
 
5
+ What's New in the LabKey 3.0.0 package
6
+ ==============================
7
+
8
+ *Release date: 12/14/2023*
9
+ - Query API - WAF encode "sql" parameter for execute_sql
10
+ - WAF encoding of parameters is initially supported with LabKey Server v23.09
11
+ - WAF encoding can be opted out of on execute_sql calls by specifying waf_encode_sql=False
12
+ - Query API - add optional parameters to insert_rows, update_rows, and delete_rows
13
+ - Query API - add move_rows()
14
+ - earliest compatible LabKey Server version: 24.1.0
15
+
16
+ What's New in the LabKey 2.6.1 package
17
+ ==============================
18
+
19
+ *Release date: 10/09/2023*
20
+ - Query API - Change max_rows default value to -1 in select_rows
21
+
5
22
  What's New in the LabKey 2.6.0 package
6
23
  ==============================
7
24
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: labkey
3
- Version: 2.6.0
3
+ Version: 3.0.0
4
4
  Summary: Python client API for LabKey Server
5
5
  Home-page: https://github.com/LabKey/labkey-api-python
6
6
  Author: LabKey
@@ -8,9 +8,7 @@ Author-email: alanv@labkey.com
8
8
  Maintainer: Alan Vezina
9
9
  Maintainer-email: alanv@labkey.com
10
10
  License: Apache License 2.0
11
- Description: Python client API for LabKey Server. Supports query and experiment APIs.
12
11
  Keywords: labkey api client
13
- Platform: UNKNOWN
14
12
  Classifier: Development Status :: 4 - Beta
15
13
  Classifier: Environment :: Console
16
14
  Classifier: Intended Audience :: Science/Research
@@ -21,4 +19,12 @@ Classifier: Operating System :: Microsoft
21
19
  Classifier: Operating System :: POSIX
22
20
  Classifier: Programming Language :: Python :: 3
23
21
  Classifier: Topic :: Scientific/Engineering
22
+ License-File: LICENSE.txt
23
+ Requires-Dist: requests
24
24
  Provides-Extra: test
25
+ Requires-Dist: pytest; extra == "test"
26
+ Requires-Dist: requests; extra == "test"
27
+ Requires-Dist: mock; extra == "test"
28
+ Requires-Dist: pytest-cov; extra == "test"
29
+
30
+ Python client API for LabKey Server. Supports query and experiment APIs.
@@ -16,6 +16,7 @@ Query API - [sample code](samples/query_examples.py)
16
16
  - **insert_rows()** - Insert rows into a table.
17
17
  - **select_rows()** - Query and get results sets.
18
18
  - **update_rows()** - Update rows in a table.
19
+ - **move_rows()()** - Move rows in a table.
19
20
  - **truncate_table()** - Delete all rows from a table.
20
21
 
21
22
  Domain API - [sample code](samples/domain_example.py)
@@ -14,6 +14,6 @@
14
14
  # limitations under the License.
15
15
  #
16
16
  __title__ = "labkey"
17
- __version__ = "2.6.0"
17
+ __version__ = "3.0.0"
18
18
  __author__ = "LabKey"
19
19
  __license__ = "Apache License 2.0"
@@ -44,6 +44,7 @@ import functools
44
44
  from typing import List
45
45
 
46
46
  from .server_context import ServerContext
47
+ from .utils import waf_encode
47
48
 
48
49
  _default_timeout = 60 * 5 # 5 minutes
49
50
 
@@ -164,12 +165,25 @@ class QueryFilter:
164
165
  return "<QueryFilter [{} {} {}]>".format(self.column_name, self.filter_type, self.value)
165
166
 
166
167
 
168
+ class AuditBehavior:
169
+ """
170
+ Enum of different auditing levels
171
+ """
172
+
173
+ DETAILED = "DETAILED"
174
+ NONE = "NONE"
175
+ SUMMARY = "SUMMARY"
176
+
177
+
167
178
  def delete_rows(
168
179
  server_context: ServerContext,
169
180
  schema_name: str,
170
181
  query_name: str,
171
182
  rows: any,
172
183
  container_path: str = None,
184
+ transacted: bool = True,
185
+ audit_behavior: AuditBehavior = None,
186
+ audit_user_comment: str = None,
173
187
  timeout: int = _default_timeout,
174
188
  ):
175
189
  """
@@ -179,12 +193,25 @@ def delete_rows(
179
193
  :param query_name: table name to delete from
180
194
  :param rows: Set of rows to delete
181
195
  :param container_path: labkey container path if not already set in context
196
+ :param transacted: whether all of the updates should be done in a single transaction
197
+ :param audit_behavior: used to override the audit behavior for the update. See class query.AuditBehavior
198
+ :param audit_user_comment: used to provide a comment that will be attached to certain detailed audit log records
182
199
  :param timeout: timeout of request in seconds (defaults to 30s)
183
200
  :return:
184
201
  """
185
202
  url = server_context.build_url("query", "deleteRows.api", container_path=container_path)
203
+
186
204
  payload = {"schemaName": schema_name, "queryName": query_name, "rows": rows}
187
205
 
206
+ if transacted is False:
207
+ payload["transacted"] = transacted
208
+
209
+ if audit_behavior is not None:
210
+ payload["auditBehavior"] = audit_behavior
211
+
212
+ if audit_user_comment is not None:
213
+ payload["auditUserComment"] = audit_user_comment
214
+
188
215
  return server_context.make_request(
189
216
  url,
190
217
  json=payload,
@@ -231,6 +258,7 @@ def execute_sql(
231
258
  parameters: dict = None,
232
259
  required_version: float = None,
233
260
  timeout: int = _default_timeout,
261
+ waf_encode_sql: bool = True
234
262
  ):
235
263
  """
236
264
  Execute sql query against a LabKey server.
@@ -248,11 +276,12 @@ def execute_sql(
248
276
  :param parameters: parameter values to pass through to a parameterized query
249
277
  :param required_version: Api version of response
250
278
  :param timeout: timeout of request in seconds (defaults to 30s)
279
+ :param waf_encode_sql: WAF encode sql in request (defaults to True)
251
280
  :return:
252
281
  """
253
282
  url = server_context.build_url("query", "executeSql.api", container_path=container_path)
254
283
 
255
- payload = {"schemaName": schema_name, "sql": sql}
284
+ payload = {"schemaName": schema_name, "sql": waf_encode(sql) if waf_encode_sql else sql}
256
285
 
257
286
  if container_filter is not None:
258
287
  payload["containerFilter"] = container_filter
@@ -285,6 +314,10 @@ def insert_rows(
285
314
  query_name: str,
286
315
  rows: List[any],
287
316
  container_path: str = None,
317
+ skip_reselect_rows: bool = False,
318
+ transacted: bool = True,
319
+ audit_behavior: AuditBehavior = None,
320
+ audit_user_comment: str = None,
288
321
  timeout: int = _default_timeout,
289
322
  ):
290
323
  """
@@ -294,6 +327,10 @@ def insert_rows(
294
327
  :param query_name: table name to insert into
295
328
  :param rows: set of rows to insert
296
329
  :param container_path: labkey container path if not already set in context
330
+ :param skip_reselect_rows: whether the full detailed response for the insert can be skipped
331
+ :param transacted: whether all of the updates should be done in a single transaction
332
+ :param audit_behavior: used to override the audit behavior for the update. See class query.AuditBehavior
333
+ :param audit_user_comment: used to provide a comment that will be attached to certain detailed audit log records
297
334
  :param timeout: timeout of request in seconds (defaults to 30s)
298
335
  :return:
299
336
  """
@@ -301,6 +338,18 @@ def insert_rows(
301
338
 
302
339
  payload = {"schemaName": schema_name, "queryName": query_name, "rows": rows}
303
340
 
341
+ if skip_reselect_rows is True:
342
+ payload["skipReselectRows"] = skip_reselect_rows
343
+
344
+ if transacted is False:
345
+ payload["transacted"] = transacted
346
+
347
+ if audit_behavior is not None:
348
+ payload["auditBehavior"] = audit_behavior
349
+
350
+ if audit_user_comment is not None:
351
+ payload["auditUserComment"] = audit_user_comment
352
+
304
353
  return server_context.make_request(
305
354
  url,
306
355
  json=payload,
@@ -316,7 +365,7 @@ def select_rows(
316
365
  filter_array: List[QueryFilter] = None,
317
366
  container_path: str = None,
318
367
  columns=None,
319
- max_rows: int = None,
368
+ max_rows: int = -1,
320
369
  sort: str = None,
321
370
  offset: int = None,
322
371
  container_filter: str = None,
@@ -339,7 +388,7 @@ def select_rows(
339
388
  :param filter_array: set of filter objects to apply
340
389
  :param container_path: folder path if not already part of server_context
341
390
  :param columns: set of columns to retrieve
342
- :param max_rows: max number of rows to retrieve
391
+ :param max_rows: max number of rows to retrieve, defaults to -1 (unlimited)
343
392
  :param sort: comma separated list of column names to sort by, prefix a column with '-' to sort descending
344
393
  :param offset: number of rows to offset results by
345
394
  :param container_filter: enumeration of the various container filters available. See:
@@ -419,6 +468,9 @@ def update_rows(
419
468
  query_name: str,
420
469
  rows: List[any],
421
470
  container_path: str = None,
471
+ transacted: bool = True,
472
+ audit_behavior: AuditBehavior = None,
473
+ audit_user_comment: str = None,
422
474
  timeout: int = _default_timeout,
423
475
  ):
424
476
  """
@@ -429,6 +481,9 @@ def update_rows(
429
481
  :param query_name: table name to update
430
482
  :param rows: Set of rows to update
431
483
  :param container_path: labkey container path if not already set in context
484
+ :param transacted: whether all of the updates should be done in a single transaction
485
+ :param audit_behavior: used to override the audit behavior for the update. See class query.AuditBehavior
486
+ :param audit_user_comment: used to provide a comment that will be attached to certain detailed audit log records
432
487
  :param timeout: timeout of request in seconds (defaults to 30s)
433
488
  :return:
434
489
  """
@@ -436,6 +491,61 @@ def update_rows(
436
491
 
437
492
  payload = {"schemaName": schema_name, "queryName": query_name, "rows": rows}
438
493
 
494
+ if transacted is False:
495
+ payload["transacted"] = transacted
496
+
497
+ if audit_behavior is not None:
498
+ payload["auditBehavior"] = audit_behavior
499
+
500
+ if audit_user_comment is not None:
501
+ payload["auditUserComment"] = audit_user_comment
502
+
503
+ return server_context.make_request(
504
+ url,
505
+ json=payload,
506
+ timeout=timeout,
507
+ )
508
+
509
+
510
+ def move_rows(
511
+ server_context: ServerContext,
512
+ target_container_path: str,
513
+ schema_name: str,
514
+ query_name: str,
515
+ rows: any,
516
+ container_path: str = None,
517
+ transacted: bool = True,
518
+ audit_behavior: AuditBehavior = None,
519
+ audit_user_comment: str = None,
520
+ timeout: int = _default_timeout,
521
+ ):
522
+ """
523
+ Move a set of rows from the schema.query
524
+ :param server_context: A LabKey server context. See utils.create_server_context.
525
+ :param target_container_path: target labkey container path for the move
526
+ :param schema_name: schema of table
527
+ :param query_name: table name to move from
528
+ :param rows: Set of rows to move
529
+ :param container_path: source labkey container path if not already set in context
530
+ :param transacted: whether all of the updates should be done in a single transaction
531
+ :param audit_behavior: used to override the audit behavior for the update. See class query.AuditBehavior
532
+ :param audit_user_comment: used to provide a comment that will be attached to certain detailed audit log records
533
+ :param timeout: timeout of request in seconds (defaults to 30s)
534
+ :return:
535
+ """
536
+ url = server_context.build_url("query", "moveRows.api", container_path=container_path)
537
+
538
+ payload = {"targetContainerPath": target_container_path, "schemaName": schema_name, "queryName": query_name, "rows": rows}
539
+
540
+ if transacted is False:
541
+ payload["transacted"] = transacted
542
+
543
+ if audit_behavior is not None:
544
+ payload["auditBehavior"] = audit_behavior
545
+
546
+ if audit_user_comment is not None:
547
+ payload["auditUserComment"] = audit_user_comment
548
+
439
549
  return server_context.make_request(
440
550
  url,
441
551
  json=payload,
@@ -458,10 +568,21 @@ class QueryWrapper:
458
568
  query_name: str,
459
569
  rows: any,
460
570
  container_path: str = None,
571
+ transacted: bool = True,
572
+ audit_behavior: AuditBehavior = None,
573
+ audit_user_comment: str = None,
461
574
  timeout: int = _default_timeout,
462
575
  ):
463
576
  return delete_rows(
464
- self.server_context, schema_name, query_name, rows, container_path, timeout
577
+ self.server_context,
578
+ schema_name,
579
+ query_name,
580
+ rows,
581
+ container_path,
582
+ transacted,
583
+ audit_behavior,
584
+ audit_user_comment,
585
+ timeout
465
586
  )
466
587
 
467
588
  @functools.wraps(truncate_table)
@@ -484,6 +605,7 @@ class QueryWrapper:
484
605
  parameters: dict = None,
485
606
  required_version: float = None,
486
607
  timeout: int = _default_timeout,
608
+ waf_encode_sql: bool = True
487
609
  ):
488
610
  return execute_sql(
489
611
  self.server_context,
@@ -498,6 +620,7 @@ class QueryWrapper:
498
620
  parameters,
499
621
  required_version,
500
622
  timeout,
623
+ waf_encode_sql
501
624
  )
502
625
 
503
626
  @functools.wraps(insert_rows)
@@ -507,10 +630,23 @@ class QueryWrapper:
507
630
  query_name: str,
508
631
  rows: List[any],
509
632
  container_path: str = None,
633
+ skip_reselect_rows: bool = False,
634
+ transacted: bool = True,
635
+ audit_behavior: AuditBehavior = None,
636
+ audit_user_comment: str = None,
510
637
  timeout: int = _default_timeout,
511
638
  ):
512
639
  return insert_rows(
513
- self.server_context, schema_name, query_name, rows, container_path, timeout
640
+ self.server_context,
641
+ schema_name,
642
+ query_name,
643
+ rows,
644
+ container_path,
645
+ skip_reselect_rows,
646
+ transacted,
647
+ audit_behavior,
648
+ audit_user_comment,
649
+ timeout
514
650
  )
515
651
 
516
652
  @functools.wraps(select_rows)
@@ -522,7 +658,7 @@ class QueryWrapper:
522
658
  filter_array: List[QueryFilter] = None,
523
659
  container_path: str = None,
524
660
  columns=None,
525
- max_rows: int = None,
661
+ max_rows: int = -1,
526
662
  sort: str = None,
527
663
  offset: int = None,
528
664
  container_filter: str = None,
@@ -566,8 +702,45 @@ class QueryWrapper:
566
702
  query_name: str,
567
703
  rows: List[any],
568
704
  container_path: str = None,
705
+ transacted: bool = True,
706
+ audit_behavior: AuditBehavior = None,
707
+ audit_user_comment: str = None,
569
708
  timeout: int = _default_timeout,
570
709
  ):
571
710
  return update_rows(
572
- self.server_context, schema_name, query_name, rows, container_path, timeout
711
+ self.server_context,
712
+ schema_name,
713
+ query_name,
714
+ rows,
715
+ container_path,
716
+ transacted,
717
+ audit_behavior,
718
+ audit_user_comment,
719
+ timeout
720
+ )
721
+
722
+ @functools.wraps(move_rows)
723
+ def move_rows(
724
+ self,
725
+ target_container_path: str,
726
+ schema_name: str,
727
+ query_name: str,
728
+ rows: any,
729
+ container_path: str = None,
730
+ transacted: bool = True,
731
+ audit_behavior: AuditBehavior = None,
732
+ audit_user_comment: str = None,
733
+ timeout: int = _default_timeout,
734
+ ):
735
+ return move_rows(
736
+ self.server_context,
737
+ target_container_path,
738
+ schema_name,
739
+ query_name,
740
+ rows,
741
+ container_path,
742
+ transacted,
743
+ audit_behavior,
744
+ audit_user_comment,
745
+ timeout
573
746
  )
@@ -16,6 +16,8 @@
16
16
  import json
17
17
  from functools import wraps
18
18
  from datetime import date, datetime
19
+ from base64 import b64encode
20
+ from urllib import parse
19
21
 
20
22
 
21
23
  # Issue #14: json.dumps on datetime throws TypeError
@@ -71,3 +73,21 @@ def transform_helper(user_transform_func, file_path_run_properties):
71
73
  row = [str(el).strip() for el in row]
72
74
  row = "\t".join(row)
73
75
  file_out.write(row + "\n")
76
+
77
+
78
+ def btoa(value: str) -> str:
79
+ if not value:
80
+ return value
81
+ binary = value.encode("utf-8")
82
+ return b64encode(binary).decode()
83
+
84
+
85
+ def encode_uri_component(value: str) -> str:
86
+ # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
87
+ return parse.quote(value, encoding="utf-8", safe="-_.!~*'()")
88
+
89
+
90
+ def waf_encode(value: str) -> str:
91
+ if value:
92
+ return "/*{{base64/x-www-form-urlencoded/wafText}}*/" + btoa(encode_uri_component(value))
93
+ return value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: labkey
3
- Version: 2.6.0
3
+ Version: 3.0.0
4
4
  Summary: Python client API for LabKey Server
5
5
  Home-page: https://github.com/LabKey/labkey-api-python
6
6
  Author: LabKey
@@ -8,9 +8,7 @@ Author-email: alanv@labkey.com
8
8
  Maintainer: Alan Vezina
9
9
  Maintainer-email: alanv@labkey.com
10
10
  License: Apache License 2.0
11
- Description: Python client API for LabKey Server. Supports query and experiment APIs.
12
11
  Keywords: labkey api client
13
- Platform: UNKNOWN
14
12
  Classifier: Development Status :: 4 - Beta
15
13
  Classifier: Environment :: Console
16
14
  Classifier: Intended Audience :: Science/Research
@@ -21,4 +19,12 @@ Classifier: Operating System :: Microsoft
21
19
  Classifier: Operating System :: POSIX
22
20
  Classifier: Programming Language :: Python :: 3
23
21
  Classifier: Topic :: Scientific/Engineering
22
+ License-File: LICENSE.txt
23
+ Requires-Dist: requests
24
24
  Provides-Extra: test
25
+ Requires-Dist: pytest; extra == "test"
26
+ Requires-Dist: requests; extra == "test"
27
+ Requires-Dist: mock; extra == "test"
28
+ Requires-Dist: pytest-cov; extra == "test"
29
+
30
+ Python client API for LabKey Server. Supports query and experiment APIs.
@@ -2,10 +2,6 @@ CHANGE.txt
2
2
  LICENSE.txt
3
3
  MANIFEST.in
4
4
  README.md
5
- download_file_example.txt
6
- lundbeck_file_download_notes.txt
7
- perf_testing_notes.txt
8
- playground.py.txt
9
5
  pyproject.toml
10
6
  pytest.ini
11
7
  setup.cfg
@@ -1,69 +0,0 @@
1
- from labkey.api_wrapper import APIWrapper
2
-
3
-
4
- def get_base_url(api: APIWrapper) -> str:
5
- ctx = api.server_context
6
-
7
- # The URL returned from LabKey Server's select rows API isn't a full URL, so we need to add The scheme
8
- # (e.g. https://), the domain, and the context path. We don't need to add the container path, because that is
9
- # already on the URL returned from the server.
10
- base_url = ctx._scheme + ctx._domain
11
-
12
- if ctx._context_path is not None:
13
- base_url += "/" + ctx._context_path
14
-
15
- return base_url
16
-
17
-
18
- def get_file(api: APIWrapper, file_url: str):
19
- """
20
- Downloads a file given a file_url from a select_rows response. File is stored in memory and returned (as bytes).
21
- Response from this function could be passed to something like pandas. This is useful when you know the file is small
22
- enough to fit into memory, but will cause problems if you have a large file (see download_file below).
23
- """
24
- ctx = api.server_context
25
- full_url = get_base_url(api) + file_url
26
- resp = ctx._session.get(full_url)
27
- return resp.content
28
-
29
-
30
- def download_file(api: APIWrapper, file_name: str, file_url: str, destination_path: str):
31
- """
32
- Downloads a file from LabKey Server to disk. This doesn't put the whole file in memory, which is good for larger
33
- files. After the file is saved to disk you can open it with another tool such as pandas.
34
- """
35
- ctx = api.server_context
36
- full_url = get_base_url(api) + file_url
37
-
38
- # This with block is needed so we clean up the connection created by requests when we're done
39
- with ctx._session.get(full_url, stream=True) as req:
40
- # This with block opens and closes the file handle for us
41
- with open(destination_path + "/" + file_name, "wb") as f:
42
- # There is no standard correct size for chunk size here, you can play around with it and see if it has an
43
- # impact on perf.
44
- for chunk in req.iter_content(chunk_size=16*1024):
45
- f.write(chunk)
46
-
47
-
48
- def main():
49
- # Create your API wrapper, the variables here will depend on your server configuration
50
- domain = "localhost:8080"
51
- container = "api_sandbox"
52
- api = APIWrapper(domain, container, use_ssl=False, verify_ssl=False)
53
- # The name of the column that is a file, this will depend on the table you're querying
54
- file_column = "file"
55
-
56
- # Select your data, using required_version=17.1 is important here, older versions of the API return the file URLs,
57
- # but not in away that are associated with the actual file column.
58
- resp = api.query.select_rows("lists", "list of files", required_version=17.1)
59
-
60
- # Here we're just grabbing the first row of data, but you could easily iterate over all of the URLs. You could even
61
- # use a thread pool to load multiple files in parallel, which would improve perf because it's I/O bound.
62
- data = resp["rows"][0]["data"]
63
- file_name = data[file_column]["value"]
64
- file_url = data[file_column]["url"]
65
- download_file(api, file_name, file_url, "./downloads")
66
-
67
-
68
- if __name__ == "__main__":
69
- main()
@@ -1,10 +0,0 @@
1
- - make assay with file field in assay results
2
- - make python script to download said file
3
- - the crux here is converting the "url" attribute of the column in the select rows response to something
4
- that our labkey api can use to download the file
5
- - Storing the file in memory should be fine
6
- - Storing in /tmp may be better because this will be done in Docker, and if it's on disk it can
7
- be streamed which should theory be better perf-wise
8
-
9
-
10
-
@@ -1,20 +0,0 @@
1
- Run User Time System Time Total CPU Time Wall Time Total Rows Rows Retrieved
2
- 1 .42 .14 .56 1:36.6 118,000 118,000
3
- 2 .38 .12 .50 5:42.9 25,000,000 26,000
4
-
5
- Hypothesis:
6
- The server is taking a long time to respond to the request, which is causing the long response time. Both scripts spend
7
- very little time running, which means they're probably spending more time waiting for data from the server. I suspect
8
- that the R script may be faster because they're possibly running on the same machine as the server, so the network
9
- request stays local to the server, whereas the Python script is being run externally from the server, so the network is
10
- necessarily slower.
11
-
12
- Steps to reproduce:
13
- 1. Create some tables to emulate the query provided by the customer
14
- 2. Populate these tables with a similar size of data (25M total rows, that can be filtered to 26k rows)
15
- 3. Run the script locally with profiler.
16
-
17
-
18
- API time: 38.3407 seconds
19
- Pandas time: 1.5355 seconds
20
- Total time: 39.8762 seconds
@@ -1,46 +0,0 @@
1
- from labkey.query import QueryFilter
2
- from labkey.api_wrapper import APIWrapper
3
-
4
-
5
- def get_webdav_url(server_context, container_path=None):
6
- parts = [server_context._scheme + server_context._domain]
7
-
8
- if server_context._context_path is not None:
9
- parts.append(server_context._context_path)
10
-
11
- parts.append("_webdav")
12
-
13
- if container_path is not None:
14
- parts.append(container_path)
15
- elif server_context._container_path is not None:
16
- parts.append(server_context._container_path)
17
-
18
- parts.append("@files")
19
- parts.append("")
20
-
21
- return "/".join(parts)
22
-
23
-
24
- def main():
25
- # Create your API wrapper, the variables here will depend on your server configuration
26
- domain = "localhost:8080"
27
- container = "NIAD Python"
28
- api = APIWrapper(domain, container, use_ssl=False, verify_ssl=False)
29
- url = get_webdav_url(api.server_context)
30
- file_name = "api_wrapper.py"
31
- file_path = "./labkey/api_wrapper.py"
32
-
33
- # Open the file you want to upload, and upload it via the webdav API
34
- with open(file_path, 'r') as file:
35
- resp = api.server_context.make_request(url, payload={"createIntermediates": 'true'}, file_payload={"file": file}, non_json_response=True)
36
-
37
- # Find the RowId of the file we just uploaded, by using select_rows and filtering by the file name.
38
- resp = api.query.select_rows('exp', 'files', filter_array=[QueryFilter('name', file_name)])
39
- row_id = resp["rows"][0]["RowId"]
40
- # Update the file metadata, the "RowId" field here is required, the rest of the fields will depend on the custom fields you have defined
41
- resp = api.query.update_rows('exp', 'files', [{ "RowId": row_id, "Site": "My Test Site", "Visit": "A Visit", "Form": "Some Form" }])
42
- print(resp)
43
-
44
-
45
- if __name__ == "__main__":
46
- main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes