labkey 2.6.1__tar.gz → 3.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.
- {labkey-2.6.1 → labkey-3.1.0}/CHANGE.txt +22 -0
- {labkey-2.6.1/labkey.egg-info → labkey-3.1.0}/PKG-INFO +9 -3
- {labkey-2.6.1 → labkey-3.1.0}/README.md +1 -0
- {labkey-2.6.1 → labkey-3.1.0}/labkey/__init__.py +1 -1
- {labkey-2.6.1 → labkey-3.1.0}/labkey/api_wrapper.py +2 -0
- {labkey-2.6.1 → labkey-3.1.0}/labkey/exceptions.py +16 -1
- {labkey-2.6.1 → labkey-3.1.0}/labkey/query.py +182 -4
- {labkey-2.6.1 → labkey-3.1.0}/labkey/security.py +1 -1
- {labkey-2.6.1 → labkey-3.1.0}/labkey/server_context.py +27 -4
- {labkey-2.6.1 → labkey-3.1.0}/labkey/utils.py +20 -0
- {labkey-2.6.1 → labkey-3.1.0/labkey.egg-info}/PKG-INFO +9 -3
- {labkey-2.6.1 → labkey-3.1.0}/labkey.egg-info/SOURCES.txt +0 -4
- labkey-2.6.1/download_file_example.txt +0 -69
- labkey-2.6.1/lundbeck_file_download_notes.txt +0 -10
- labkey-2.6.1/perf_testing_notes.txt +0 -20
- labkey-2.6.1/playground.py.txt +0 -46
- {labkey-2.6.1 → labkey-3.1.0}/LICENSE.txt +0 -0
- {labkey-2.6.1 → labkey-3.1.0}/MANIFEST.in +0 -0
- {labkey-2.6.1 → labkey-3.1.0}/labkey/container.py +0 -0
- {labkey-2.6.1 → labkey-3.1.0}/labkey/domain.py +0 -0
- {labkey-2.6.1 → labkey-3.1.0}/labkey/experiment.py +0 -0
- {labkey-2.6.1 → labkey-3.1.0}/labkey/storage.py +0 -0
- {labkey-2.6.1 → labkey-3.1.0}/labkey.egg-info/dependency_links.txt +0 -0
- {labkey-2.6.1 → labkey-3.1.0}/labkey.egg-info/requires.txt +0 -0
- {labkey-2.6.1 → labkey-3.1.0}/labkey.egg-info/top_level.txt +0 -0
- {labkey-2.6.1 → labkey-3.1.0}/pyproject.toml +0 -0
- {labkey-2.6.1 → labkey-3.1.0}/pytest.ini +0 -0
- {labkey-2.6.1 → labkey-3.1.0}/setup.cfg +0 -0
- {labkey-2.6.1 → labkey-3.1.0}/setup.py +0 -0
@@ -2,6 +2,28 @@
|
|
2
2
|
LabKey Python Client API News
|
3
3
|
+++++++++++
|
4
4
|
|
5
|
+
What's New in the LabKey 3.1.0 package
|
6
|
+
==============================
|
7
|
+
|
8
|
+
*Release date: 04/03/2024*
|
9
|
+
- ServerContext
|
10
|
+
- Add allow_redirects flag (defaults to False) to constructor
|
11
|
+
- Add allow_redirects flag to make_request
|
12
|
+
- APIWrapper: Add allow_redirects flag (defaults to False)
|
13
|
+
- Add UnexpectedRedirectError
|
14
|
+
- thrown when allow_redirects is False and the server issues a redirect
|
15
|
+
|
16
|
+
What's New in the LabKey 3.0.0 package
|
17
|
+
==============================
|
18
|
+
|
19
|
+
*Release date: 12/14/2023*
|
20
|
+
- Query API - WAF encode "sql" parameter for execute_sql
|
21
|
+
- WAF encoding of parameters is initially supported with LabKey Server v23.09
|
22
|
+
- WAF encoding can be opted out of on execute_sql calls by specifying waf_encode_sql=False
|
23
|
+
- Query API - add optional parameters to insert_rows, update_rows, and delete_rows
|
24
|
+
- Query API - add move_rows()
|
25
|
+
- earliest compatible LabKey Server version: 24.1.0
|
26
|
+
|
5
27
|
What's New in the LabKey 2.6.1 package
|
6
28
|
==============================
|
7
29
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: labkey
|
3
|
-
Version:
|
3
|
+
Version: 3.1.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)
|
@@ -22,6 +22,7 @@ class APIWrapper:
|
|
22
22
|
verify_ssl=True,
|
23
23
|
api_key=None,
|
24
24
|
disable_csrf=False,
|
25
|
+
allow_redirects=False,
|
25
26
|
):
|
26
27
|
self.server_context = ServerContext(
|
27
28
|
domain=domain,
|
@@ -31,6 +32,7 @@ class APIWrapper:
|
|
31
32
|
verify_ssl=verify_ssl,
|
32
33
|
api_key=api_key,
|
33
34
|
disable_csrf=disable_csrf,
|
35
|
+
allow_redirects=allow_redirects,
|
34
36
|
)
|
35
37
|
self.container = ContainerWrapper(self.server_context)
|
36
38
|
self.domain = DomainWrapper(self.server_context)
|
@@ -47,7 +47,22 @@ class RequestError(exceptions.RequestException):
|
|
47
47
|
self.message = "No response received"
|
48
48
|
|
49
49
|
def __str__(self):
|
50
|
-
return
|
50
|
+
return str(self.message)
|
51
|
+
|
52
|
+
|
53
|
+
class UnexpectedRedirectError(RequestError):
|
54
|
+
default_msg = "Unexpected redirect occurred"
|
55
|
+
|
56
|
+
def __init__(self, server_response, **kwargs):
|
57
|
+
super().__init__(server_response, **kwargs)
|
58
|
+
|
59
|
+
location = server_response.headers.get("Location", "")
|
60
|
+
|
61
|
+
# If the server is redirecting from http to https the user probably has a misconfigured ServerContext with use_ssl=False
|
62
|
+
if server_response.url.startswith("http://") and location.startswith("https://"):
|
63
|
+
self.message = "Redirected from http to https, set use_ssl=True in your APIWrapper or ServerContext"
|
64
|
+
elif location != "":
|
65
|
+
self.message = f"Unexpected redirect to: {location}"
|
51
66
|
|
52
67
|
|
53
68
|
class QueryNotFoundError(RequestError):
|
@@ -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,
|
@@ -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,66 @@ 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 = {
|
539
|
+
"targetContainerPath": target_container_path,
|
540
|
+
"schemaName": schema_name,
|
541
|
+
"queryName": query_name,
|
542
|
+
"rows": rows,
|
543
|
+
}
|
544
|
+
|
545
|
+
if transacted is False:
|
546
|
+
payload["transacted"] = transacted
|
547
|
+
|
548
|
+
if audit_behavior is not None:
|
549
|
+
payload["auditBehavior"] = audit_behavior
|
550
|
+
|
551
|
+
if audit_user_comment is not None:
|
552
|
+
payload["auditUserComment"] = audit_user_comment
|
553
|
+
|
439
554
|
return server_context.make_request(
|
440
555
|
url,
|
441
556
|
json=payload,
|
@@ -458,10 +573,21 @@ class QueryWrapper:
|
|
458
573
|
query_name: str,
|
459
574
|
rows: any,
|
460
575
|
container_path: str = None,
|
576
|
+
transacted: bool = True,
|
577
|
+
audit_behavior: AuditBehavior = None,
|
578
|
+
audit_user_comment: str = None,
|
461
579
|
timeout: int = _default_timeout,
|
462
580
|
):
|
463
581
|
return delete_rows(
|
464
|
-
self.server_context,
|
582
|
+
self.server_context,
|
583
|
+
schema_name,
|
584
|
+
query_name,
|
585
|
+
rows,
|
586
|
+
container_path,
|
587
|
+
transacted,
|
588
|
+
audit_behavior,
|
589
|
+
audit_user_comment,
|
590
|
+
timeout,
|
465
591
|
)
|
466
592
|
|
467
593
|
@functools.wraps(truncate_table)
|
@@ -484,6 +610,7 @@ class QueryWrapper:
|
|
484
610
|
parameters: dict = None,
|
485
611
|
required_version: float = None,
|
486
612
|
timeout: int = _default_timeout,
|
613
|
+
waf_encode_sql: bool = True,
|
487
614
|
):
|
488
615
|
return execute_sql(
|
489
616
|
self.server_context,
|
@@ -498,6 +625,7 @@ class QueryWrapper:
|
|
498
625
|
parameters,
|
499
626
|
required_version,
|
500
627
|
timeout,
|
628
|
+
waf_encode_sql,
|
501
629
|
)
|
502
630
|
|
503
631
|
@functools.wraps(insert_rows)
|
@@ -507,10 +635,23 @@ class QueryWrapper:
|
|
507
635
|
query_name: str,
|
508
636
|
rows: List[any],
|
509
637
|
container_path: str = None,
|
638
|
+
skip_reselect_rows: bool = False,
|
639
|
+
transacted: bool = True,
|
640
|
+
audit_behavior: AuditBehavior = None,
|
641
|
+
audit_user_comment: str = None,
|
510
642
|
timeout: int = _default_timeout,
|
511
643
|
):
|
512
644
|
return insert_rows(
|
513
|
-
self.server_context,
|
645
|
+
self.server_context,
|
646
|
+
schema_name,
|
647
|
+
query_name,
|
648
|
+
rows,
|
649
|
+
container_path,
|
650
|
+
skip_reselect_rows,
|
651
|
+
transacted,
|
652
|
+
audit_behavior,
|
653
|
+
audit_user_comment,
|
654
|
+
timeout,
|
514
655
|
)
|
515
656
|
|
516
657
|
@functools.wraps(select_rows)
|
@@ -566,8 +707,45 @@ class QueryWrapper:
|
|
566
707
|
query_name: str,
|
567
708
|
rows: List[any],
|
568
709
|
container_path: str = None,
|
710
|
+
transacted: bool = True,
|
711
|
+
audit_behavior: AuditBehavior = None,
|
712
|
+
audit_user_comment: str = None,
|
569
713
|
timeout: int = _default_timeout,
|
570
714
|
):
|
571
715
|
return update_rows(
|
572
|
-
self.server_context,
|
716
|
+
self.server_context,
|
717
|
+
schema_name,
|
718
|
+
query_name,
|
719
|
+
rows,
|
720
|
+
container_path,
|
721
|
+
transacted,
|
722
|
+
audit_behavior,
|
723
|
+
audit_user_comment,
|
724
|
+
timeout,
|
725
|
+
)
|
726
|
+
|
727
|
+
@functools.wraps(move_rows)
|
728
|
+
def move_rows(
|
729
|
+
self,
|
730
|
+
target_container_path: str,
|
731
|
+
schema_name: str,
|
732
|
+
query_name: str,
|
733
|
+
rows: any,
|
734
|
+
container_path: str = None,
|
735
|
+
transacted: bool = True,
|
736
|
+
audit_behavior: AuditBehavior = None,
|
737
|
+
audit_user_comment: str = None,
|
738
|
+
timeout: int = _default_timeout,
|
739
|
+
):
|
740
|
+
return move_rows(
|
741
|
+
self.server_context,
|
742
|
+
target_container_path,
|
743
|
+
schema_name,
|
744
|
+
query_name,
|
745
|
+
rows,
|
746
|
+
container_path,
|
747
|
+
transacted,
|
748
|
+
audit_behavior,
|
749
|
+
audit_user_comment,
|
750
|
+
timeout,
|
573
751
|
)
|
@@ -279,7 +279,7 @@ def stop_impersonating(server_context: ServerContext):
|
|
279
279
|
Stop impersonating a user while keeping the original user logged in.
|
280
280
|
"""
|
281
281
|
url = server_context.build_url(LOGIN_CONTROLLER, "stopImpersonating.api")
|
282
|
-
return server_context.make_request(url)
|
282
|
+
return server_context.make_request(url, allow_redirects=True)
|
283
283
|
|
284
284
|
|
285
285
|
@dataclass
|
@@ -8,6 +8,7 @@ from labkey.exceptions import (
|
|
8
8
|
QueryNotFoundError,
|
9
9
|
ServerContextError,
|
10
10
|
ServerNotFoundError,
|
11
|
+
UnexpectedRedirectError,
|
11
12
|
)
|
12
13
|
|
13
14
|
API_KEY_TOKEN = "apikey"
|
@@ -29,7 +30,8 @@ def handle_response(response, non_json_response=False):
|
|
29
30
|
content=response.content,
|
30
31
|
)
|
31
32
|
return result
|
32
|
-
|
33
|
+
elif sc == 302:
|
34
|
+
raise UnexpectedRedirectError(response)
|
33
35
|
elif sc == 401:
|
34
36
|
raise RequestAuthorizationError(response)
|
35
37
|
elif sc == 404:
|
@@ -62,6 +64,7 @@ class ServerContext:
|
|
62
64
|
verify_ssl=True,
|
63
65
|
api_key=None,
|
64
66
|
disable_csrf=False,
|
67
|
+
allow_redirects=False,
|
65
68
|
):
|
66
69
|
self._container_path = container_path
|
67
70
|
self._context_path = context_path
|
@@ -70,6 +73,7 @@ class ServerContext:
|
|
70
73
|
self._verify_ssl = verify_ssl
|
71
74
|
self._api_key = api_key
|
72
75
|
self._disable_csrf = disable_csrf
|
76
|
+
self.allow_redirects = allow_redirects
|
73
77
|
self._session = requests.Session()
|
74
78
|
self._session.headers.update({"User-Agent": f"LabKey Python API/{__version__}"})
|
75
79
|
|
@@ -174,7 +178,9 @@ class ServerContext:
|
|
174
178
|
non_json_response: bool = False,
|
175
179
|
file_payload: any = None,
|
176
180
|
json: dict = None,
|
181
|
+
allow_redirects=False,
|
177
182
|
) -> any:
|
183
|
+
allow_redirects_ = allow_redirects or self.allow_redirects
|
178
184
|
if self._api_key is not None:
|
179
185
|
if self._session.headers.get(API_KEY_TOKEN) is not self._api_key:
|
180
186
|
self._session.headers.update({API_KEY_TOKEN: self._api_key})
|
@@ -189,7 +195,13 @@ class ServerContext:
|
|
189
195
|
|
190
196
|
try:
|
191
197
|
if method == "GET":
|
192
|
-
response = self._session.get(
|
198
|
+
response = self._session.get(
|
199
|
+
url,
|
200
|
+
params=payload,
|
201
|
+
headers=headers,
|
202
|
+
timeout=timeout,
|
203
|
+
allow_redirects=allow_redirects_,
|
204
|
+
)
|
193
205
|
else:
|
194
206
|
if file_payload is not None:
|
195
207
|
response = self._session.post(
|
@@ -198,6 +210,7 @@ class ServerContext:
|
|
198
210
|
files=file_payload,
|
199
211
|
headers=headers,
|
200
212
|
timeout=timeout,
|
213
|
+
allow_redirects=allow_redirects_,
|
201
214
|
)
|
202
215
|
elif json is not None:
|
203
216
|
if headers is None:
|
@@ -206,10 +219,20 @@ class ServerContext:
|
|
206
219
|
headers_ = {**headers, "Content-Type": "application/json"}
|
207
220
|
# sort_keys is a hack to make unit tests work
|
208
221
|
data = json_dumps(json, sort_keys=True)
|
209
|
-
response = self._session.post(
|
222
|
+
response = self._session.post(
|
223
|
+
url,
|
224
|
+
data=data,
|
225
|
+
headers=headers_,
|
226
|
+
timeout=timeout,
|
227
|
+
allow_redirects=allow_redirects_,
|
228
|
+
)
|
210
229
|
else:
|
211
230
|
response = self._session.post(
|
212
|
-
url,
|
231
|
+
url,
|
232
|
+
data=payload,
|
233
|
+
headers=headers,
|
234
|
+
timeout=timeout,
|
235
|
+
allow_redirects=allow_redirects_,
|
213
236
|
)
|
214
237
|
return handle_response(response, non_json_response)
|
215
238
|
except RequestException as e:
|
@@ -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.1.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.1/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
|