Skip to content

Commit d736304

Browse files
authored
Add get_queries to query module (#90)
* Add get_queries to query module and query API wrapper * Add get_queries integration test * Add get_queries unit tests * Refactor unit tests to use pytest instead of unittest
1 parent 40a8030 commit d736304

File tree

14 files changed

+1892
-1985
lines changed

14 files changed

+1892
-1985
lines changed

CHANGE.txt

Lines changed: 7 additions & 0 deletions
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 4.2.0 package
6+
==============================
7+
8+
*Release date: 02/26/2026*
9+
- Add get_queries to query module
10+
- Accessible via API wrapper as api.query.get_queries
11+
512
What's New in the LabKey 4.1.0 package
613
==============================
714

labkey/query.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@
4040
4141
############################################################################
4242
"""
43+
4344
import functools
4445
from typing import List, Literal, NotRequired, TextIO, TypedDict
4546

4647
from .server_context import ServerContext
47-
from .utils import waf_encode
48+
from .utils import waf_encode, transform_options
4849

4950
_default_timeout = 60 * 5 # 5 minutes
5051

@@ -60,6 +61,7 @@ class Pagination:
6061
ALL = "all"
6162
NONE = "none"
6263

64+
6365
# TODO: Provide filter generators.
6466
#
6567
# There are some inconsistencies between the different filter types with multiple values,
@@ -143,7 +145,6 @@ class Types:
143145
ARRAY_ISEMPTY = "arrayisempty"
144146
ARRAY_ISNOTEMPTY = "arrayisnotempty"
145147

146-
147148
# Table/Query-wise operators
148149
Q = "q"
149150

@@ -704,6 +705,53 @@ def move_rows(
704705
)
705706

706707

708+
get_queries_fields = [
709+
"schema_name",
710+
"include_columns",
711+
"include_system_queries",
712+
"include_title",
713+
"include_user_queries",
714+
"include_view_data_url",
715+
"query_detail_columns",
716+
]
717+
718+
719+
def get_queries(
720+
server_context: ServerContext,
721+
schema_name: str,
722+
container_path: str = None,
723+
timeout=_default_timeout,
724+
**kwargs,
725+
) -> dict:
726+
"""
727+
:param server_context: A LabKey server context. See utils.create_server_context.
728+
:param schema_name: schema of table
729+
:param container_path: folder path if not already part of server_context
730+
:param timeout: Request timeout in seconds (defaults to 300s)
731+
:param kwargs: Optional parameters supported by this API:
732+
include_columns: boolean, if set to False, information about the available columns in this query will not be
733+
included in the results. Default is True.
734+
include_system_queries: boolean, if set to false, system-defined queries will not be included in the results.
735+
Default is True.
736+
include_title: boolean, if set to False, no custom query titles will be included. Instead, titles will be
737+
identical to names. Default is True.
738+
include_user_queries: boolean, if set to False, user-defined queries will not be included in the results.
739+
Default is True.
740+
include_view_data_url: boolean, if set to False, view data URLs will not be included in the results.
741+
Default is True.
742+
query_detail_columns: boolean, if set to True, and includeColumns is set to True, information about the
743+
available columns will be the same details as specified by getQueryDetails for columns. Defaults to False.
744+
:return: dict
745+
"""
746+
url = server_context.build_url("query", "getQueries.api", container_path=container_path)
747+
payload = {"schemaName": schema_name}
748+
749+
if len(kwargs) > 0:
750+
payload = {**payload, **transform_options(kwargs, get_queries_fields)}
751+
752+
return server_context.make_request(url, payload, timeout=timeout)
753+
754+
707755
class QueryWrapper:
708756
"""
709757
Wrapper for all of the API methods exposed in the query module. Used by the APIWrapper class.
@@ -939,3 +987,29 @@ def move_rows(
939987
audit_user_comment,
940988
timeout,
941989
)
990+
991+
@functools.wraps(get_queries)
992+
def get_queries(
993+
self,
994+
schema_name: str,
995+
container_path: str = None,
996+
include_columns: bool = None,
997+
include_system_queries: bool = None,
998+
include_title: bool = None,
999+
include_user_queries: bool = None,
1000+
include_view_data_url: bool = None,
1001+
query_detail_columns: bool = None,
1002+
timeout=_default_timeout,
1003+
):
1004+
return get_queries(
1005+
self.server_context,
1006+
schema_name,
1007+
container_path,
1008+
timeout,
1009+
include_columns=include_columns,
1010+
include_system_queries=include_system_queries,
1011+
include_title=include_title,
1012+
include_user_queries=include_user_queries,
1013+
include_view_data_url=include_view_data_url,
1014+
query_detail_columns=query_detail_columns,
1015+
)

labkey/server_context.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,6 @@ def __init__(
7979
self._session = requests.Session()
8080
self._session.headers.update({"User-Agent": f"LabKey Python API/{client_version}"})
8181

82-
print(f"User Agent header: LabKey Python API/{client_version}")
83-
8482
if self._use_ssl:
8583
self._scheme = "https://"
8684
if not self._verify_ssl:

labkey/storage.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
4747
############################################################################
4848
"""
49+
4950
import functools
5051
from dataclasses import dataclass
5152

labkey/utils.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from functools import wraps
1818
from datetime import date, datetime
1919
from base64 import b64encode
20+
from typing import List
2021
from urllib import parse
2122

2223

@@ -91,3 +92,34 @@ def waf_encode(value: str) -> str:
9192
if value:
9293
return "/*{{base64/x-www-form-urlencoded/wafText}}*/" + btoa(encode_uri_component(value))
9394
return value
95+
96+
97+
def snake_to_camel(value: str):
98+
"""
99+
Converts a snake_case string to camelCase
100+
"""
101+
if not value:
102+
return value
103+
104+
if "_" not in value:
105+
return value
106+
107+
parts = [part for part in value.split("_") if part]
108+
109+
if len(parts) == 0:
110+
return ""
111+
112+
return parts[0].lower() + "".join([part.title() for part in parts[1:]])
113+
114+
115+
def transform_options(options: dict, expected_keys: List[str]) -> dict:
116+
"""
117+
Converts a dict with snake_case keys to a new dict with camelCase keys, only copying keys from expected_keys
118+
"""
119+
transformed_options = {}
120+
121+
for key, item in options.items():
122+
if key in expected_keys:
123+
transformed_options[snake_to_camel(key)] = item
124+
125+
return transformed_options

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name="labkey"
7-
version = "4.1.0"
7+
version = "4.2.0"
88
description = "Python client API for LabKey Server"
99
dependencies = ["requests>=2.32.5"]
1010
readme = "README.md"

samples/query_examples.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"""
1717
Examples using the Query.py API
1818
"""
19+
1920
from labkey.api_wrapper import APIWrapper
2021
from labkey.exceptions import (
2122
RequestError,

test/integration/test_query.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,8 @@ def test_api_save_rows(api: APIWrapper, blood_sample_type_fixture, tissue_sample
346346
assert resp["committed"] == False
347347
assert resp["errorCount"] == 1
348348
assert (
349-
"SampleID or Name is required for sample on row 2" in
350-
resp["result"][0]["errors"]["exception"]
349+
"SampleID or Name is required for sample on row 2"
350+
in resp["result"][0]["errors"]["exception"]
351351
)
352352

353353
# Fix the first command by specifying a name for the sample
@@ -411,3 +411,45 @@ def test_api_save_rows(api: APIWrapper, blood_sample_type_fixture, tissue_sample
411411
assert resp["result"][2]["rowsAffected"] == 1
412412
assert resp["result"][2]["rows"][0]["rowid"] == first_tissue_row_id
413413
assert resp["result"][2]["rows"][0]["receiveddate"] == "2025-07-07 12:34:56.000"
414+
415+
416+
expected_fields = {
417+
"canEdit",
418+
"canEditSharedViews",
419+
"columns",
420+
"hidden",
421+
"inherit",
422+
"isIncludedForLookups",
423+
"isInherited",
424+
"isMetadataOverrideable",
425+
"isUserDefined",
426+
"moduleName",
427+
"name",
428+
"snapshot",
429+
"title",
430+
"viewDataUrl",
431+
}
432+
433+
434+
def test_get_queries(api: APIWrapper):
435+
resp = api.query.get_queries("core")
436+
437+
all_queries_count = len(resp["queries"])
438+
assert set(resp.keys()) == {"schemaName", "queries"}
439+
assert resp["schemaName"] == "core"
440+
assert all_queries_count > 0
441+
assert set(resp["queries"][0].keys()) == set(expected_fields)
442+
443+
resp = api.query.get_queries("core", include_system_queries=False, include_user_queries=False)
444+
445+
assert set(resp.keys()) == {"schemaName", "queries"}
446+
assert resp["schemaName"] == "core"
447+
# By excluding system queries, and user queries, we should have no queries
448+
assert len(resp["queries"]) == 0
449+
450+
resp = api.query.get_queries("core", include_columns=False, include_view_data_url=False)
451+
452+
assert set(resp["queries"][0].keys()) == expected_fields - {
453+
"columns",
454+
"viewDataUrl",
455+
}

0 commit comments

Comments
 (0)