Skip to content

Commit caa481c

Browse files
authored
Add save_rows to Query API (#83)
1 parent c06ac6a commit caa481c

File tree

7 files changed

+475
-20
lines changed

7 files changed

+475
-20
lines changed

CHANGE.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
LabKey Python Client API News
33
+++++++++++
44

5+
What's New in the LabKey 4.0.0 package
6+
==============================
7+
8+
*Release date: 07/08/2025*
9+
- Add save_rows API to query module
10+
- Accessible via API wrappers e.g. api.query.save_rows
11+
- Update the minimum required version of Python to 3.10
12+
513
What's New in the LabKey 3.4.0 package
614
==============================
715

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ else:
113113
```
114114

115115
## Supported Versions
116-
Python 3.7+ is fully supported.
116+
Python 3.10+ is fully supported. <!-- Note: update setup.py python_requires if you change this -->
117117
LabKey Server v15.1 and later.
118118

119119
## Contributing

labkey/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
# limitations under the License.
1515
#
1616
__title__ = "labkey"
17-
__version__ = "3.4.0"
17+
__version__ = "4.0.0"
1818
__author__ = "LabKey"
1919
__license__ = "Apache License 2.0"

labkey/query.py

Lines changed: 122 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@
1515
#
1616
"""
1717
############################################################################
18-
NAME:
19-
LabKey Query API
18+
NAME:
19+
LabKey Query API
2020
21-
SUMMARY:
21+
SUMMARY:
2222
This module provides functions for interacting with data on a LabKey Server.
2323
2424
DESCRIPTION:
25-
This module is designed to simplify querying and manipulating data in LabKey Server.
26-
Its APIs are modeled after the LabKey Server JavaScript APIs of the same names.
25+
This module is designed to simplify querying and manipulating data in LabKey Server.
26+
Its APIs are modeled after the LabKey Server JavaScript APIs of the same names.
2727
2828
Installation and Setup for the LabKey Python API:
2929
https://github.com/LabKey/labkey-api-python/blob/master/README.md
@@ -41,7 +41,7 @@
4141
############################################################################
4242
"""
4343
import functools
44-
from typing import List, TextIO
44+
from typing import List, Literal, NotRequired, TextIO, TypedDict
4545

4646
from .server_context import ServerContext
4747
from .utils import waf_encode
@@ -196,7 +196,7 @@ def delete_rows(
196196
:param transacted: whether all of the updates should be done in a single transaction
197197
:param audit_behavior: used to override the audit behavior for the update. See class query.AuditBehavior
198198
:param audit_user_comment: used to provide a comment that will be attached to certain detailed audit log records
199-
:param timeout: timeout of request in seconds (defaults to 30s)
199+
:param timeout: timeout of request in seconds (defaults to 300s)
200200
:return:
201201
"""
202202
url = server_context.build_url("query", "deleteRows.api", container_path=container_path)
@@ -232,7 +232,7 @@ def truncate_table(
232232
:param schema_name: schema of table
233233
:param query_name: table name to delete from
234234
:param container_path: labkey container path if not already set in context
235-
:param timeout: timeout of request in seconds (defaults to 30s)
235+
:param timeout: timeout of request in seconds (defaults to 300s)
236236
:return:
237237
"""
238238
url = server_context.build_url("query", "truncateTable.api", container_path=container_path)
@@ -275,7 +275,7 @@ def execute_sql(
275275
:param save_in_session: save query result as a named view to the session
276276
:param parameters: parameter values to pass through to a parameterized query
277277
:param required_version: Api version of response
278-
:param timeout: timeout of request in seconds (defaults to 30s)
278+
:param timeout: timeout of request in seconds (defaults to 300s)
279279
:param waf_encode_sql: WAF encode sql in request (defaults to True)
280280
:return:
281281
"""
@@ -331,7 +331,7 @@ def insert_rows(
331331
:param transacted: whether all of the updates should be done in a single transaction
332332
:param audit_behavior: used to override the audit behavior for the update. See class query.AuditBehavior
333333
:param audit_user_comment: used to provide a comment that will be attached to certain detailed audit log records
334-
:param timeout: timeout of request in seconds (defaults to 30s)
334+
:param timeout: timeout of request in seconds (defaults to 300s)
335335
:return:
336336
"""
337337
url = server_context.build_url("query", "insertRows.api", container_path=container_path)
@@ -407,6 +407,93 @@ def import_rows(
407407
return server_context.make_request(url, payload, method="POST", file_payload=file_payload)
408408

409409

410+
class Command(TypedDict):
411+
"""
412+
TypedDict representing a command for saveRows API.
413+
"""
414+
415+
audit_behavior: NotRequired[AuditBehavior]
416+
audit_user_comment: NotRequired[str]
417+
command: Literal["insert", "update", "delete"]
418+
container_path: NotRequired[str]
419+
extra_context: NotRequired[dict]
420+
query_name: str
421+
rows: List[any]
422+
schema_name: str
423+
skip_reselect_rows: NotRequired[bool]
424+
425+
426+
def save_rows(
427+
server_context: ServerContext,
428+
commands: List[Command],
429+
api_version: float = None,
430+
container_path: str = None,
431+
extra_context: dict = None,
432+
timeout: int = _default_timeout,
433+
transacted: bool = None,
434+
validate_only: bool = None,
435+
):
436+
"""
437+
Save inserts, updates, and/or deletes to potentially multiple tables with a single request.
438+
:param server_context: A LabKey server context. See utils.create_server_context.
439+
:param commands: A List of the update/insert/delete operations to be performed.
440+
:param api_version: decimal value that indicates the response version of the api. If this is 13.2 or higher, a
441+
request that fails validation will be returned as a successful response. Use the 'errorCount' and 'committed'
442+
properties in the response to tell if it committed or not.
443+
:param container_path: folder path if not already part of server_context
444+
:param extra_context: Extra context object passed into the transformation/validation script environment.
445+
:param timeout: Request timeout in seconds (defaults to 300s)
446+
:param transacted: Whether all the commands should be done in a single transaction, so they all succeed or all
447+
fail. Defaults to true.
448+
:param validate_only: Whether the server should attempt to proceed through all the commands but not commit them to
449+
the database. Useful for scenarios like giving incremental validation feedback as a user fills out a UI form but
450+
does not save anything until they explicitly request a save.
451+
"""
452+
url = server_context.build_url("query", "saveRows.api", container_path=container_path)
453+
454+
json_commands = []
455+
for command in commands:
456+
json_command = {
457+
"command": command["command"],
458+
"queryName": command["query_name"],
459+
"schemaName": command["schema_name"],
460+
"rows": command["rows"],
461+
}
462+
463+
if command.get("audit_behavior") is not None:
464+
json_command["auditBehavior"] = command["audit_behavior"]
465+
466+
if command.get("audit_user_comment") is not None:
467+
json_command["auditUserComment"] = command["audit_user_comment"]
468+
469+
if command.get("container_path") is not None:
470+
json_command["containerPath"] = command["container_path"]
471+
472+
if command.get("extra_context") is not None:
473+
json_command["extraContext"] = command["extra_context"]
474+
475+
if command.get("skip_reselect_rows") is not None:
476+
json_command["skipReselectRows"] = command["skip_reselect_rows"]
477+
478+
json_commands.append(json_command)
479+
480+
payload = {"commands": json_commands}
481+
482+
if api_version is not None:
483+
payload["apiVersion"] = api_version
484+
485+
if extra_context is not None:
486+
payload["extraContext"] = extra_context
487+
488+
if transacted is not None:
489+
payload["transacted"] = transacted
490+
491+
if validate_only is not None:
492+
payload["validateOnly"] = validate_only
493+
494+
return server_context.make_request(url, json=payload, timeout=timeout)
495+
496+
410497
def select_rows(
411498
server_context: ServerContext,
412499
schema_name: str,
@@ -450,7 +537,7 @@ def select_rows(
450537
:param include_update_column: Boolean value that indicates whether to include an Update link column in results
451538
:param selection_key:
452539
:param required_version: decimal value that indicates the response version of the api
453-
:param timeout: Request timeout in seconds (defaults to 30s)
540+
:param timeout: Request timeout in seconds (defaults to 300s)
454541
:param ignore_filter: Boolean, if true, the command will ignore any filter that may be part of the chosen view.
455542
:return:
456543
"""
@@ -534,7 +621,7 @@ def update_rows(
534621
:param transacted: whether all of the updates should be done in a single transaction
535622
:param audit_behavior: used to override the audit behavior for the update. See class query.AuditBehavior
536623
:param audit_user_comment: used to provide a comment that will be attached to certain detailed audit log records
537-
:param timeout: timeout of request in seconds (defaults to 30s)
624+
:param timeout: timeout of request in seconds (defaults to 300s)
538625
:return:
539626
"""
540627
url = server_context.build_url("query", "updateRows.api", container_path=container_path)
@@ -580,7 +667,7 @@ def move_rows(
580667
:param transacted: whether all of the updates should be done in a single transaction
581668
:param audit_behavior: used to override the audit behavior for the update. See class query.AuditBehavior
582669
:param audit_user_comment: used to provide a comment that will be attached to certain detailed audit log records
583-
:param timeout: timeout of request in seconds (defaults to 30s)
670+
:param timeout: timeout of request in seconds (defaults to 300s)
584671
:return:
585672
"""
586673
url = server_context.build_url("query", "moveRows.api", container_path=container_path)
@@ -726,6 +813,28 @@ def import_rows(
726813
import_lookup_by_alternate_key,
727814
)
728815

816+
@functools.wraps(save_rows)
817+
def save_rows(
818+
self,
819+
commands: List[Command],
820+
api_version: float = None,
821+
container_path: str = None,
822+
extra_context: dict = None,
823+
timeout: int = _default_timeout,
824+
transacted: bool = None,
825+
validate_only: bool = None,
826+
):
827+
return save_rows(
828+
self.server_context,
829+
commands,
830+
api_version,
831+
container_path,
832+
extra_context,
833+
timeout,
834+
transacted,
835+
validate_only,
836+
)
837+
729838
@functools.wraps(select_rows)
730839
def select_rows(
731840
self,

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
url="https://github.com/LabKey/labkey-api-python",
5050
packages=packages,
5151
package_data={},
52+
python_requires=">=3.10", # Note: update README.md supported versions if you change this
5253
install_requires=["requests"],
5354
extras_require={"test": tests_require, "dev": dev_require, "build": build_require},
5455
keywords="labkey api client",

0 commit comments

Comments
 (0)