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.
- {labkey-2.6.0 → labkey-3.0.0}/CHANGE.txt +17 -0
- {labkey-2.6.0/labkey.egg-info → labkey-3.0.0}/PKG-INFO +9 -3
- {labkey-2.6.0 → labkey-3.0.0}/README.md +1 -0
- {labkey-2.6.0 → labkey-3.0.0}/labkey/__init__.py +1 -1
- {labkey-2.6.0 → labkey-3.0.0}/labkey/query.py +180 -7
- {labkey-2.6.0 → labkey-3.0.0}/labkey/utils.py +20 -0
- {labkey-2.6.0 → labkey-3.0.0/labkey.egg-info}/PKG-INFO +9 -3
- {labkey-2.6.0 → labkey-3.0.0}/labkey.egg-info/SOURCES.txt +0 -4
- labkey-2.6.0/download_file_example.txt +0 -69
- labkey-2.6.0/lundbeck_file_download_notes.txt +0 -10
- labkey-2.6.0/perf_testing_notes.txt +0 -20
- labkey-2.6.0/playground.py.txt +0 -46
- {labkey-2.6.0 → labkey-3.0.0}/LICENSE.txt +0 -0
- {labkey-2.6.0 → labkey-3.0.0}/MANIFEST.in +0 -0
- {labkey-2.6.0 → labkey-3.0.0}/labkey/api_wrapper.py +0 -0
- {labkey-2.6.0 → labkey-3.0.0}/labkey/container.py +0 -0
- {labkey-2.6.0 → labkey-3.0.0}/labkey/domain.py +0 -0
- {labkey-2.6.0 → labkey-3.0.0}/labkey/exceptions.py +0 -0
- {labkey-2.6.0 → labkey-3.0.0}/labkey/experiment.py +0 -0
- {labkey-2.6.0 → labkey-3.0.0}/labkey/security.py +0 -0
- {labkey-2.6.0 → labkey-3.0.0}/labkey/server_context.py +0 -0
- {labkey-2.6.0 → labkey-3.0.0}/labkey/storage.py +0 -0
- {labkey-2.6.0 → labkey-3.0.0}/labkey.egg-info/dependency_links.txt +0 -0
- {labkey-2.6.0 → labkey-3.0.0}/labkey.egg-info/requires.txt +0 -0
- {labkey-2.6.0 → labkey-3.0.0}/labkey.egg-info/top_level.txt +0 -0
- {labkey-2.6.0 → labkey-3.0.0}/pyproject.toml +0 -0
- {labkey-2.6.0 → labkey-3.0.0}/pytest.ini +0 -0
- {labkey-2.6.0 → labkey-3.0.0}/setup.cfg +0 -0
- {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:
|
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)
|
@@ -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 =
|
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,
|
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,
|
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 =
|
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,
|
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:
|
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.
|
@@ -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
|
labkey-2.6.0/playground.py.txt
DELETED
@@ -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
|
File without changes
|
File without changes
|
File without changes
|