Skip to content

Commit 1602b14

Browse files
authored
[MPT-18938] Added render() as queryable mixin (#226)
This pull request introduces support for a new `render()` query parameter in the API client, allowing users to request rendered output in their queries. The implementation includes updates to the core query-building logic, a new method for chaining `render()` into queries, and comprehensive tests to ensure correctness. Enhancements to query parameter handling: * Added a `with_render` boolean option to the `QueryState` class, allowing the inclusion of the `render()` parameter in the query string when building API requests. (`mpt_api_client/http/query_state.py`) [[1]](diffhunk://#diff-1d40a2820baa57fd0d1a32181811a6269490c4ebe0d01a81e2092753e230a035R20-R34) [[2]](diffhunk://#diff-1d40a2820baa57fd0d1a32181811a6269490c4ebe0d01a81e2092753e230a035R51-R55) [[3]](diffhunk://#diff-1d40a2820baa57fd0d1a32181811a6269490c4ebe0d01a81e2092753e230a035R78-R84) * Introduced the `with_render()` method in `QueryableMixin`, enabling method chaining to easily add the `render()` parameter to queries. (`mpt_api_client/http/mixins/queryable_mixin.py`) Testing and validation: * Added end-to-end tests for both async and sync clients to verify that records returned with `render()` do not contain template characters and that the correct object is returned. (`tests/e2e/audit/records/test_async_records.py`, `tests/e2e/audit/records/test_sync_records.py`) [[1]](diffhunk://#diff-d5824e2487473d7b2d7b6d429cb542a344a8e292af159e30c2db414e273507c4R45-R55) [[2]](diffhunk://#diff-485960b9be8eac93a315313579525f281be8ed7106f8364dfb5ce40c83e43f11R46-R56) * Created unit tests for the new `with_render()` method and for query string building with and without `render()`. (`tests/unit/http/mixins/test_queryable_mixin.py`, `tests/unit/http/test_query_state.py`) [[1]](diffhunk://#diff-e42182fb74793b73637a4e1d86903f0c68b5bc2ab85edb80c2d8cb413df329a6R81-R88) [[2]](diffhunk://#diff-1e8de208c42e77b791fcd52302bf69b5cb1cbd4107cc40e320015c08425b0964R26) [[3]](diffhunk://#diff-1e8de208c42e77b791fcd52302bf69b5cb1cbd4107cc40e320015c08425b0964R38-R55) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> Closes [MPT-18938](https://softwareone.atlassian.net/browse/MPT-18938) - Add support for requesting rendered output via a render() query parameter - Extend QueryState with a with_render flag/property; QueryState.build() appends render() when enabled - Add with_render() to QueryableMixin to enable chaining render() into queries and propagate the flag through filter/order_by/select - Add unit tests for QueryState.with_render and QueryableMixin chaining behavior - Add sync and async end-to-end tests verifying rendered records contain no template delimiters and return the expected objects <!-- end of auto-generated comment: release notes by coderabbit.ai --> [MPT-18938]: https://softwareone.atlassian.net/browse/MPT-18938?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2 parents baca528 + 5de0d7f commit 1602b14

File tree

6 files changed

+84
-0
lines changed

6 files changed

+84
-0
lines changed

mpt_api_client/http/mixins/queryable_mixin.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def order_by(self, *fields: str) -> Self:
2323
rql=self.query_state.filter, # type: ignore[attr-defined]
2424
order_by=list(fields),
2525
select=self.query_state.select, # type: ignore[attr-defined]
26+
render=self.query_state.render, # type: ignore[attr-defined]
2627
)
2728
)
2829

@@ -39,6 +40,7 @@ def filter(self, rql: RQLQuery) -> Self:
3940
rql=combined_filter,
4041
order_by=self.query_state.order_by, # type: ignore[attr-defined]
4142
select=self.query_state.select, # type: ignore[attr-defined]
43+
render=self.query_state.render, # type: ignore[attr-defined]
4244
)
4345
)
4446

@@ -60,6 +62,22 @@ def select(self, *fields: str) -> Self:
6062
rql=self.query_state.filter, # type: ignore[attr-defined]
6163
order_by=self.query_state.order_by, # type: ignore[attr-defined]
6264
select=list(fields),
65+
render=self.query_state.render, # type: ignore[attr-defined]
66+
),
67+
)
68+
69+
def options(self, *, render: bool = False) -> Self:
70+
"""Set query options.
71+
72+
Returns:
73+
New copy of the collection with the given options applied.
74+
"""
75+
return self._create_new_instance(
76+
QueryState(
77+
rql=self.query_state.filter, # type: ignore[attr-defined]
78+
order_by=self.query_state.order_by, # type: ignore[attr-defined]
79+
select=self.query_state.select, # type: ignore[attr-defined]
80+
render=render,
6381
),
6482
)
6583

mpt_api_client/http/query_state.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,21 @@ def __init__(
1717
rql: RQLQuery | None = None,
1818
order_by: list[str] | None = None,
1919
select: list[str] | None = None,
20+
*,
21+
render: bool = False,
2022
) -> None:
2123
"""Initialize the query state with optional filter, ordering, and selection criteria.
2224
2325
Args:
2426
rql: RQL query for filtering data.
2527
order_by: List of fields to order by (prefix with '-' for descending).
2628
select: List of fields to select in the response.
29+
render: Whether to include the render() parameter in the query string.
2730
"""
2831
self._filter = rql
2932
self._order_by = order_by
3033
self._select = select
34+
self._render = render
3135

3236
@property
3337
def filter(self) -> RQLQuery | None:
@@ -44,6 +48,11 @@ def select(self) -> list[str] | None:
4448
"""Get the current select fields."""
4549
return self._select
4650

51+
@property
52+
def render(self) -> bool:
53+
"""Get the current render state."""
54+
return self._render
55+
4756
def build(self, query_params: dict[str, Any] | None = None) -> str:
4857
"""Build a query string from the current state and additional parameters.
4958
@@ -66,7 +75,11 @@ def build(self, query_params: dict[str, Any] | None = None) -> str:
6675
if self._filter:
6776
query_parts.append(str(self._filter))
6877

78+
if self._render:
79+
query_parts.append("render()")
80+
6981
if query_parts:
7082
query = "&".join(query_parts)
7183
return f"{query}"
84+
7285
return ""

tests/e2e/audit/records/test_async_records.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,15 @@ async def test_get_record_not_found(async_mpt_vendor: AsyncMPTClient) -> None:
4242

4343
with pytest.raises(MPTAPIError):
4444
await service.get("REC-000-000-000")
45+
46+
47+
async def test_get_records_with_render(async_mpt_vendor: AsyncMPTClient, product_id: str) -> None:
48+
template_chars = ["{{", "}}"]
49+
audit_filter = RQLQuery(object__id=product_id)
50+
service = async_mpt_vendor.audit.records.filter(audit_filter).options(render=True)
51+
records = [record async for record in service.iterate()]
52+
53+
assert records
54+
for record in records:
55+
assert record.object.id == product_id
56+
assert not any(char in record.details for char in template_chars)

tests/e2e/audit/records/test_sync_records.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,16 @@ def test_get_record_not_found(mpt_vendor: MPTClient) -> None:
4343

4444
with pytest.raises(MPTAPIError):
4545
service.get("REC-000-000-000")
46+
47+
48+
def test_get_records_with_render(mpt_vendor: MPTClient, product_id) -> None:
49+
template_chars = ["{{", "}}"]
50+
audit_filter = RQLQuery(object__id=product_id)
51+
service = mpt_vendor.audit.records.filter(audit_filter).options(render=True)
52+
53+
result = list(service.iterate())
54+
55+
assert result
56+
for record in result:
57+
assert record.object.id == product_id
58+
assert not any(char in record.details for char in template_chars)

tests/unit/http/mixins/test_queryable_mixin.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,12 @@ def test_queryable_mixin_method_chaining(
7878
assert result.query_state.filter == filter_status_active
7979
assert result.query_state.order_by == ["created", "-name"]
8080
assert result.query_state.select == ["id", "name"]
81+
82+
83+
def test_queryable_mixin_options_render(dummy_service: DummyService) -> None:
84+
result = dummy_service.options(render=True)
85+
86+
assert result != dummy_service
87+
assert not dummy_service.query_state.render
88+
assert result.query_state.render
89+
assert result.select("id").query_state.render

tests/unit/http/test_query_state.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def test_build_url(filter_status_active):
2323
rql=filter_status_active,
2424
select=["-audit", "product.agreements", "-product.agreements.product"],
2525
order_by=["-created", "name"],
26+
render=False,
2627
)
2728

2829
result = query_state.build()
@@ -34,6 +35,24 @@ def test_build_url(filter_status_active):
3435
)
3536

3637

38+
def test_build_url_with_render(filter_status_active):
39+
query_state = QueryState(
40+
rql=filter_status_active,
41+
select=["-audit", "product.agreements", "-product.agreements.product"],
42+
order_by=["-created", "name"],
43+
render=True,
44+
)
45+
46+
result = query_state.build()
47+
48+
assert result == (
49+
"order=-created,name"
50+
"&select=-audit,product.agreements,-product.agreements.product"
51+
"&eq(status,'active')"
52+
"&render()"
53+
)
54+
55+
3756
def test_empty_build(query_state):
3857
result = query_state.build()
3958

0 commit comments

Comments
 (0)