Skip to content

Commit f2037cd

Browse files
authored
Add import_rows to Query API (#78)
1 parent c2f2cda commit f2037cd

File tree

6 files changed

+187
-5
lines changed

6 files changed

+187
-5
lines changed

CHANGE.txt

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
LabKey Python Client API News
33
+++++++++++
44

5+
What's New in the LabKey 3.3.0 package
6+
==============================
7+
8+
*Release date: 12/3/2024*
9+
- Add import_rows API to query module
10+
- Accessible via API wrappers e.g. api.query.import_rows
11+
512
What's New in the LabKey 3.2.0 package
613
==============================
714

labkey/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
# limitations under the License.
1515
#
1616
__title__ = "labkey"
17-
__version__ = "3.2.0"
17+
__version__ = "3.3.0"
1818
__author__ = "LabKey"
1919
__license__ = "Apache License 2.0"

labkey/domain.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# limitations under the License.
1515
#
1616
import functools
17-
from typing import Dict, List, Union, Tuple
17+
from typing import Dict, List, Union, Tuple, TextIO
1818

1919
from .server_context import ServerContext
2020
from labkey.query import QueryFilter
@@ -483,7 +483,7 @@ def get_domain_details(
483483

484484

485485
def infer_fields(
486-
server_context: ServerContext, data_file: any, container_path: str = None
486+
server_context: ServerContext, data_file: TextIO, container_path: str = None
487487
) -> List[PropertyDescriptor]:
488488
"""
489489
Infer fields for a domain from a file

labkey/query.py

+73-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
############################################################################
4242
"""
4343
import functools
44-
from typing import List
44+
from typing import List, TextIO
4545

4646
from .server_context import ServerContext
4747
from .utils import waf_encode
@@ -357,6 +357,56 @@ def insert_rows(
357357
)
358358

359359

360+
def import_rows(
361+
server_context: ServerContext,
362+
schema_name: str,
363+
query_name: str,
364+
data_file: TextIO,
365+
container_path: str = None,
366+
insert_option: str = None,
367+
audit_behavior: str = None,
368+
import_lookup_by_alternate_key: bool = False,
369+
):
370+
"""
371+
Import row(s) into a table
372+
:param server_context: A LabKey server context. See utils.create_server_context.
373+
:param schema_name: schema of table
374+
:param query_name: table name to import into
375+
:param data_file: the file containing the rows to import. The column names in the file must match the column names
376+
from the LabKey server.
377+
:param container_path: labkey container path if not already set in context
378+
:param insert_option: Whether the import action should be done as an insert, creating new rows for each provided row
379+
of the data frame, or a merge. When merging during import, any data you provide for the rows representing records
380+
that already exist will replace the previous values. Note that when updating an existing record, you only need to
381+
provide the columns you wish to update, existing data for other columns will be left as is. Available options are
382+
"INSERT" and "MERGE". Defaults to "INSERT".
383+
:param audit_behavior: Set the level of auditing details for this import action. Available options are "SUMMARY" and
384+
"DETAILED". SUMMARY - Audit log reflects that a change was made, but does not mention the nature of the change.
385+
DETAILED - Provides full details on what change was made, including values before and after the change. Defaults to
386+
the setting as specified by the LabKey query.
387+
:param import_lookup_by_alternate_key: Allows lookup target rows to be resolved by values rather than the target's
388+
primary key. This option will only be available for lookups that are configured with unique column information
389+
:return:
390+
"""
391+
url = server_context.build_url("query", "import.api", container_path=container_path)
392+
file_payload = {"file": data_file}
393+
payload = {
394+
"schemaName": schema_name,
395+
"queryName": query_name,
396+
}
397+
398+
if insert_option is not None:
399+
payload["insertOption"] = insert_option
400+
401+
if audit_behavior is not None:
402+
payload["auditBehavior"] = audit_behavior
403+
404+
if import_lookup_by_alternate_key is not None:
405+
payload["importLookupByAlternateKey"] = import_lookup_by_alternate_key
406+
407+
return server_context.make_request(url, payload, method="POST", file_payload=file_payload)
408+
409+
360410
def select_rows(
361411
server_context: ServerContext,
362412
schema_name: str,
@@ -654,6 +704,28 @@ def insert_rows(
654704
timeout,
655705
)
656706

707+
@functools.wraps(import_rows)
708+
def import_rows(
709+
self,
710+
schema_name: str,
711+
query_name: str,
712+
data_file,
713+
container_path: str = None,
714+
insert_option: str = None,
715+
audit_behavior: str = None,
716+
import_lookup_by_alternate_key: bool = False,
717+
):
718+
return import_rows(
719+
self.server_context,
720+
schema_name,
721+
query_name,
722+
data_file,
723+
container_path,
724+
insert_option,
725+
audit_behavior,
726+
import_lookup_by_alternate_key,
727+
)
728+
657729
@functools.wraps(select_rows)
658730
def select_rows(
659731
self,

labkey/server_context.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import Dict, TextIO
12
from labkey.utils import json_dumps
23
from . import __version__
34
import requests
@@ -176,7 +177,7 @@ def make_request(
176177
timeout: int = 300,
177178
method: str = "POST",
178179
non_json_response: bool = False,
179-
file_payload: any = None,
180+
file_payload: Dict[str, TextIO] = None,
180181
json: dict = None,
181182
allow_redirects=False,
182183
) -> any:

test/integration/test_query.py

+102
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,105 @@ def test_cannot_delete_qc_state_in_use(api: APIWrapper, qc_states, study, datase
157157
# now clean up/stop using it
158158
dataset_row_to_remove = [{"lsid": inserted_lsid}]
159159
api.query.delete_rows(SCHEMA_NAME, QUERY_NAME, dataset_row_to_remove)
160+
161+
LISTS_SCHEMA = "lists"
162+
PARENT_LIST_NAME = "parent_list"
163+
PARENT_LIST_DEFINITION = {
164+
"kind": "IntList",
165+
"domainDesign": {
166+
"name": PARENT_LIST_NAME,
167+
"fields": [
168+
{"name": "rowId", "rangeURI": "int"},
169+
{
170+
"name": "name",
171+
"rangeURI": "string",
172+
"required": True,
173+
},
174+
],
175+
},
176+
"indices": {
177+
"columnNames": ["name"],
178+
"unique": True,
179+
},
180+
"options": {"keyName": "rowId", "keyType": "AutoIncrementInteger"},
181+
}
182+
CHILD_LIST_NAME = "child_list"
183+
CHILD_LIST_DEFINITION = {
184+
"kind": "IntList",
185+
"domainDesign": {
186+
"name": CHILD_LIST_NAME,
187+
"fields": [
188+
{"name": "rowId", "rangeURI": "int"},
189+
{
190+
"name": "name",
191+
"rangeURI": "string",
192+
"required": True,
193+
},
194+
{
195+
"name": "parent",
196+
"lookupQuery": "parent_list",
197+
"lookupSchema": "lists",
198+
"rangeURI": "int",
199+
},
200+
],
201+
},
202+
"options": {"keyName": "rowId", "keyType": "AutoIncrementInteger"},
203+
}
204+
205+
parent_data = """name
206+
parent_one
207+
parent_two
208+
parent_three
209+
"""
210+
211+
child_data = """name,parent
212+
child_one,parent_one
213+
child_two,parent_two
214+
child_three,parent_three
215+
"""
216+
217+
@pytest.fixture
218+
def parent_list_fixture(api: APIWrapper):
219+
api.domain.create(PARENT_LIST_DEFINITION)
220+
created_list = api.domain.get(LISTS_SCHEMA, PARENT_LIST_NAME)
221+
yield created_list
222+
# clean up
223+
api.domain.drop(LISTS_SCHEMA, PARENT_LIST_NAME)
224+
225+
226+
@pytest.fixture
227+
def child_list_fixture(api: APIWrapper):
228+
api.domain.create(CHILD_LIST_DEFINITION)
229+
created_list = api.domain.get(LISTS_SCHEMA, CHILD_LIST_NAME)
230+
yield created_list
231+
# clean up
232+
api.domain.drop(LISTS_SCHEMA, CHILD_LIST_NAME)
233+
234+
235+
def test_import_rows(api: APIWrapper, parent_list_fixture, child_list_fixture, tmpdir):
236+
parent_data_path = tmpdir.join("parent_data.csv")
237+
parent_data_path.write(parent_data)
238+
child_data_path = tmpdir.join("child_data.csv")
239+
child_data_path.write(child_data)
240+
241+
# Should succeed
242+
parent_file = parent_data_path.open()
243+
resp = api.query.import_rows("lists", PARENT_LIST_NAME, data_file=parent_file)
244+
parent_file.close()
245+
assert resp["success"] == True
246+
assert resp["rowCount"] == 3
247+
248+
# Should fail, because data doesn't use rowIds and import_lookup_by_alternate_key defaults to False
249+
child_file = child_data_path.open()
250+
resp = api.query.import_rows("lists", CHILD_LIST_NAME, data_file=child_file)
251+
child_file.close()
252+
assert resp["success"] == False
253+
assert resp["errorCount"] == 1
254+
assert resp["errors"][0]["exception"] == "Could not convert value 'parent_one' (String) for Integer field 'parent'"
255+
256+
# Should pass, because import_lookup_by_alternate_key is True
257+
child_file = child_data_path.open()
258+
resp = api.query.import_rows("lists", CHILD_LIST_NAME, data_file=child_file, import_lookup_by_alternate_key=True)
259+
child_file.close()
260+
assert resp["success"] == True
261+
assert resp["rowCount"] == 3

0 commit comments

Comments
 (0)