Skip to content

Commit 75cac39

Browse files
author
labkey-ians
committed
Spec 24062: Support Python 3
- Adding additional parameter support for query.py - Added query_examples.py
1 parent 69b2d94 commit 75cac39

File tree

6 files changed

+350
-54
lines changed

6 files changed

+350
-54
lines changed

README.md

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,37 @@ $ pip install labkey
99
```
1010

1111
# Credentials
12-
In order to the use the Python client API for LabKey Server, you will need to specify your login credentials in a credential file. The package assumes that this file will be located either:
12+
The api no longer supports using a ``.labkeycredentials.txt`` file, and now uses the .netrc files similar to the other labkey apis. Additional .netrc [setup instructions](https://www.labkey.org/wiki/Staff/steveh/DocsSandbox/page.view?name=netrc&_docid=wiki%3Ae780ab5b-241e-1033-93dd-22a830bccfbb) can be found at the link.
1313

14-
1. ``$HOME/.labkeycredentials.txt``
15-
2. The location will be specified in the ``LABKEY_CREDENTIALS`` environment variable.
14+
## Set Up a netrc File
1615

17-
The ``labkeycredentials`` file must be in the following format. (3 separate lines):
16+
On a Mac, UNIX, or Linux system the netrc file should be named ``.netrc`` (dot netrc) and on Windows it should be named ``_netrc`` (underscore netrc). The file should be located in your home directory and the permissions on the file must be set so that you are the only user who can read it, i.e. it is unreadable to everyone else.
17+
18+
To create the netrc on a Windows machine, first create an environment variable called ’HOME’ that is set to your home directory (c:/Users/<User-Name> on Vista or Windows 7) or any directory you want to use.
19+
20+
In that directory, create a text file with the prefix appropriate to your system, either an underscore or dot.
21+
22+
The following three lines must be included in the file. The lines must be separated by either white space (spaces, tabs, or newlines) or commas:
1823
```
19-
machine https://hosted.labkey.com
20-
login labkeypython@gmail.com
21-
password python
24+
machine <remote-instance-of-labkey-server>
25+
login <user-email>
26+
password <user-password>
2227
```
23-
where:
24-
- machine: URL of your LabKey Server
25-
- login: email address to be used to login to the LabKey Server
26-
- password: password associated with the login
2728

28-
A sample ``labkeycredentials`` file has been shipped with the source and named ``.labkeycredentials.sample``.
29+
One example would be:
30+
```
31+
machine mymachine.labkey.org
32+
login user@labkey.org
33+
password mypassword
34+
```
35+
Another example would be:
36+
```
37+
machine mymachine.labkey.org login user@labkey.org password mypassword
38+
```
2939

3040
# Supported Versions
31-
Python 2.6 or 2.7 are fully supported.
41+
Python 2.6 or 2.7 and 3.4 are fully supported.
42+
3243
LabKey Server v11.1 and later.
3344

3445
# Contributing

labkey/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
#
16+
from labkey import query, experiment # wiki, messageboard
17+
1618
__title__ = 'labkey'
1719
__version__ = '0.3.0'
1820
__author__ = 'LabKey Software'
1921
__license__ = 'Apache 2.0'
20-
21-
from labkey import query # wiki, messageboard

labkey/query.py

Lines changed: 136 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,28 @@
4545
############################################################################
4646
"""
4747
from __future__ import unicode_literals
48-
from enum import Enum
48+
import json
4949

5050
from requests.exceptions import SSLError
5151
from labkey.utils import build_url, handle_response
5252

5353

54-
class Pagination(Enum):
55-
paginated = 0
56-
selected = 1
57-
unselected = 2
58-
all = 3
59-
none = 4
54+
_query_headers = {
55+
'Content-Type': 'application/json'
56+
}
6057

61-
def delete_rows(server_context, schema_name, query_name, rows, container_path=None):
58+
_default_timeout = 30 # in seconds
59+
60+
61+
class Pagination:
62+
PAGINATED = 'paginated'
63+
SELECTED = 'selected'
64+
UNSELECTED = 'unselected'
65+
ALL = 'all'
66+
NONE = 'none'
67+
68+
69+
def delete_rows(server_context, schema_name, query_name, rows, container_path=None, transacted=None, timeout=_default_timeout):
6270
url = build_url(server_context, 'query', 'deleteRows.api', container_path=container_path)
6371

6472
payload = {
@@ -67,12 +75,20 @@ def delete_rows(server_context, schema_name, query_name, rows, container_path=No
6775
'rows': rows
6876
}
6977

70-
delete_rows_response = _make_request(server_context, url, payload)
78+
# explicit json payload and headers required for form generation
79+
delete_rows_response = _make_request(server_context, url, json.dumps(payload), headers=_query_headers, timeout=timeout)
7180
return delete_rows_response
7281

7382

7483
def execute_sql(server_context, schema_name, sql, container_path=None,
75-
max_rows=None, sort=None, offset=None, container_filter=None):
84+
max_rows=None,
85+
sort=None,
86+
offset=None,
87+
container_filter=None,
88+
save_in_session=None,
89+
parameters=None,
90+
required_version=None,
91+
timeout=_default_timeout):
7692
url = build_url(server_context, 'query', 'executeSql.api', container_path=container_path)
7793

7894
payload = {
@@ -81,22 +97,31 @@ def execute_sql(server_context, schema_name, sql, container_path=None,
8197
}
8298

8399
if container_filter is not None:
84-
payload['query.containerFilter'] = container_filter
100+
payload['containerFilter'] = container_filter
85101

86102
if max_rows is not None:
87-
payload['query.max_rows'] = max_rows
103+
payload['maxRows'] = max_rows
88104

89105
if offset is not None:
90-
payload['query.offset'] = offset
106+
payload['offset'] = offset
91107

92108
if sort is not None:
93109
payload['query.sort'] = sort
94110

95-
execute_sql_response = _make_request(server_context, url, payload)
111+
if save_in_session is not None:
112+
payload['saveInSession'] = save_in_session
113+
114+
if parameters is not None:
115+
payload['query.parameters'] = parameters
116+
117+
if required_version is not None:
118+
payload['apiVersion'] = required_version
119+
120+
execute_sql_response = _make_request(server_context, url, payload, timeout=timeout)
96121
return execute_sql_response
97122

98123

99-
def insert_rows(server_context, schema_name, query_name, rows, container_path=None):
124+
def insert_rows(server_context, schema_name, query_name, rows, container_path=None, timeout=_default_timeout):
100125
url = build_url(server_context, 'query', 'insertRows.api', container_path=container_path)
101126

102127
payload = {
@@ -105,11 +130,11 @@ def insert_rows(server_context, schema_name, query_name, rows, container_path=No
105130
'rows': rows
106131
}
107132

108-
insert_rows_response = _make_request(server_context, url, payload)
133+
# explicit json payload and headers required for form generation
134+
insert_rows_response = _make_request(server_context, url, json.dumps(payload), headers=_query_headers, timeout=timeout)
109135
return insert_rows_response
110136

111137

112-
# TODO: Support all the properties
113138
def select_rows(server_context, schema_name, query_name, view_name=None,
114139
filter_array=None,
115140
container_path=None,
@@ -123,9 +148,9 @@ def select_rows(server_context, schema_name, query_name, view_name=None,
123148
include_total_count=None,
124149
include_details_column=None,
125150
include_update_column=None,
126-
# selection_key=None,
127-
timeout=None,
128-
required_version=None
151+
selection_key=None,
152+
required_version=None,
153+
timeout=_default_timeout
129154
):
130155
# TODO: Support data_region_name
131156
url = build_url(server_context, 'query', 'getQuery.api', container_path=container_path)
@@ -140,9 +165,9 @@ def select_rows(server_context, schema_name, query_name, view_name=None,
140165
payload['query.viewName'] = view_name
141166

142167
if filter_array is not None:
143-
for filter in filter_array:
144-
prefix = 'query.' + filter[0] + '~' + filter[1]
145-
payload[prefix] = filter[2]
168+
for query_filter in filter_array:
169+
prefix = query_filter.get_url_parameter_name()
170+
payload[prefix] = query_filter.get_url_parameter_value()
146171

147172
if columns is not None:
148173
payload['query.columns'] = columns
@@ -157,7 +182,7 @@ def select_rows(server_context, schema_name, query_name, view_name=None,
157182
payload['query.offset'] = offset
158183

159184
if container_filter is not None:
160-
payload['query.containerFilter'] = container_filter
185+
payload['containerFilter'] = container_filter
161186

162187
if parameters is not None:
163188
payload['query.parameters'] = parameters
@@ -166,25 +191,25 @@ def select_rows(server_context, schema_name, query_name, view_name=None,
166191
payload['query.showRows'] = show_rows
167192

168193
if include_total_count is not None:
169-
payload['query.includeTotalCount'] = include_total_count
194+
payload['includeTotalCount'] = include_total_count
170195

171196
if include_details_column is not None:
172-
payload['query.includeDetailsColumn'] = include_details_column
197+
payload['includeDetailsColumn'] = include_details_column
173198

174199
if include_update_column is not None:
175-
payload['query.includeUpdateColumn'] = include_update_column
200+
payload['includeUpdateColumn'] = include_update_column
176201

177-
if timeout is not None:
178-
payload['query.timeout'] = timeout
202+
if selection_key is not None:
203+
payload['query.selectionKey'] = selection_key
179204

180205
if required_version is not None:
181-
payload['query.requiredVersion'] = required_version
206+
payload['apiVersion'] = required_version
182207

183-
select_rows_response = _make_request(server_context, url, payload)
208+
select_rows_response = _make_request(server_context, url, payload, timeout=timeout)
184209
return select_rows_response
185210

186211

187-
def update_rows(server_context, schema_name, query_name, rows, container_path=None):
212+
def update_rows(server_context, schema_name, query_name, rows, container_path=None, timeout=_default_timeout):
188213
url = build_url(server_context, 'query', 'updateRows.api', container_path=container_path)
189214

190215
payload = {
@@ -193,14 +218,90 @@ def update_rows(server_context, schema_name, query_name, rows, container_path=No
193218
'rows': rows
194219
}
195220

196-
update_rows_response = _make_request(server_context, url, payload)
221+
# explicit json payload and headers required for form generation
222+
update_rows_response = _make_request(server_context, url, json.dumps(payload), headers=_query_headers, timeout=timeout)
197223
return update_rows_response
198224

199225

200-
def _make_request(server_context, url, payload):
226+
def _make_request(server_context, url, payload, headers=None, timeout=_default_timeout):
201227
try:
202228
session = server_context['session']
203-
raw_response = session.post(url, data=payload)
229+
raw_response = session.post(url, data=payload, headers=headers, timeout=timeout)
204230
return handle_response(raw_response)
205231
except SSLError as e:
206232
raise Exception('Failed to match server SSL configuration. Ensure the server_context is configured correctly.')
233+
234+
235+
# TODO: Provide filter generators.
236+
#
237+
# There are some inconsistencies between the different filter types with multiple values,
238+
# some use ';' and others use ',' to delimit values within string list; and still others use an array of value objects.
239+
# This is a historical artifact of the api and isn't clearly documented.
240+
#
241+
# https://www.labkey.org/download/clientapi_docs/javascript-api/symbols/LABKEY.Filter.html
242+
class QueryFilter:
243+
244+
class Types:
245+
HAS_ANY_VALUE = '',
246+
247+
EQUAL = 'eq',
248+
DATE_EQUAL = 'dateeq',
249+
250+
NEQ = 'neq',
251+
NOT_EQUAL = 'neq',
252+
DATE_NOT_EQUAL = 'dateneq',
253+
254+
NEQ_OR_NULL = 'neqornull',
255+
NOT_EQUAL_OR_MISSING = 'neqornull',
256+
257+
GT = 'gt',
258+
GREATER_THAN = 'gt',
259+
DATE_GREATER_THAN = 'dategt',
260+
261+
LT = 'lt',
262+
LESS_THAN = 'lt',
263+
DATE_LESS_THAN = 'datelt',
264+
265+
GTE = 'gte',
266+
GREATER_THAN_OR_EQUAL = 'gte',
267+
DATE_GREATER_THAN_OR_EQUAL = 'dategte',
268+
269+
LTE = 'lte',
270+
LESS_THAN_OR_EQUAL = 'lte',
271+
DATE_LESS_THAN_OR_EQUAL = 'datelte',
272+
273+
STARTS_WITH = 'startswith',
274+
DOES_NOT_START_WITH = 'doesnotstartwith',
275+
276+
CONTAINS = 'contains',
277+
DOES_NOT_CONTAIN = 'doesnotcontain',
278+
279+
CONTAINS_ONE_OF = 'containsoneof',
280+
CONTAINS_NONE_OF = 'containsnoneof',
281+
282+
IN = 'in',
283+
284+
EQUALS_ONE_OF = 'in',
285+
286+
NOT_IN = 'notin',
287+
EQUALS_NONE_OF = 'notin',
288+
289+
BETWEEN = 'between',
290+
NOT_BETWEEN = 'notbetween',
291+
292+
MEMBER_OF = 'memberof'
293+
294+
def __init__(self, column, value, filter_type = Types.EQUAL):
295+
self.column_name = column
296+
self.value = value
297+
self.filter_type = filter_type
298+
299+
def get_url_parameter_name(self):
300+
return 'query.' + self.column_name + '~' + self.filter_type[0]
301+
302+
def get_url_parameter_value(self):
303+
return self.value
304+
305+
def get_column_name(self):
306+
return self.column_name
307+

samples/.labkeycredentials.sample

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)