Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions mpt_api_client/http/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from mpt_api_client.constants import APPLICATION_JSON
from mpt_api_client.exceptions import MPTError, transform_http_status_exception
from mpt_api_client.http.client import json_to_file_payload
from mpt_api_client.http.client_utils import validate_base_url
from mpt_api_client.http.client_utils import get_query_params, validate_base_url
from mpt_api_client.http.query_options import QueryOptions
from mpt_api_client.http.types import (
HeaderTypes,
QueryParam,
Expand Down Expand Up @@ -63,6 +64,7 @@ async def request( # noqa: WPS211
headers: HeaderTypes | None = None,
json_file_key: str = "_attachment_data",
force_multipart: bool = False,
options: QueryOptions | None = None,
) -> Response:
"""Perform an HTTP request.

Expand All @@ -75,6 +77,7 @@ async def request( # noqa: WPS211
headers: Request headers.
json_file_key: json file name for data when sending a multipart request.
force_multipart: force multipart request even if file is not provided.
options: Additional options for the request.

Returns:
Response object.
Expand All @@ -88,13 +91,14 @@ async def request( # noqa: WPS211
if force_multipart or (files and json):
files[json_file_key] = (None, json_to_file_payload(json), APPLICATION_JSON)
json = None
params_str = get_query_params(query_params, options)
try:
response = await self.httpx_client.request(
method,
url,
files=files,
json=json,
params=query_params,
params=params_str or None,
headers=headers,
)
except HTTPError as err:
Expand Down
8 changes: 6 additions & 2 deletions mpt_api_client/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
MPTError,
transform_http_status_exception,
)
from mpt_api_client.http.client_utils import validate_base_url
from mpt_api_client.http.client_utils import get_query_params, validate_base_url
from mpt_api_client.http.query_options import QueryOptions
from mpt_api_client.http.types import (
HeaderTypes,
QueryParam,
Expand Down Expand Up @@ -76,6 +77,7 @@ def request( # noqa: WPS211
headers: HeaderTypes | None = None,
json_file_key: str = "_attachment_data",
force_multipart: bool = False,
options: QueryOptions | None = None,
) -> Response:
"""Perform an HTTP request.

Expand All @@ -88,6 +90,7 @@ def request( # noqa: WPS211
headers: Request headers.
json_file_key: json file name for data when sending a multipart request.
force_multipart: force multipart request even if file is not provided.
options: Additional options for the request.

Returns:
Response object.
Expand All @@ -101,13 +104,14 @@ def request( # noqa: WPS211
if force_multipart or (files and json):
files[json_file_key] = (None, json_to_file_payload(json), APPLICATION_JSON)
json = None
params_str = get_query_params(query_params, options)
try:
response = self.httpx_client.request(
method,
url,
files=files,
json=json,
params=query_params,
params=params_str or None,
headers=headers,
)
except HTTPError as err:
Expand Down
22 changes: 21 additions & 1 deletion mpt_api_client/http/client_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import re
from urllib.parse import SplitResult, urlsplit, urlunparse
from typing import Any
from urllib.parse import SplitResult, urlencode, urlsplit, urlunparse

from mpt_api_client.http.query_options import QueryOptions

INVALID_ENV_URL_MESSAGE = (
"Base URL is required. "
Expand Down Expand Up @@ -45,3 +48,20 @@ def validate_base_url(base_url: str | None) -> str:
raise ValueError(INVALID_ENV_URL_MESSAGE)

return _build_sanitized_base_url(split_result)


def get_query_params(
query_params: dict[str, Any] | None, options: QueryOptions | None = None
) -> str:
"""Get query params string from dict."""
filtered_params = {
query_param: query_value
for query_param, query_value in (query_params or {}).items()
if query_value is not None
}

query_params_str = urlencode(filtered_params) if filtered_params else ""
if options and options.render:
query_params_str += "&render()" if query_params_str else "render()"

return query_params_str
11 changes: 8 additions & 3 deletions mpt_api_client/http/mixins/get_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ def get(self, resource_id: str, select: list[str] | str | None = None) -> Model:
"""
if isinstance(select, list):
select = ",".join(select) if select else None

return self._resource(resource_id).get(query_params={"select": select}) # type: ignore[attr-defined, no-any-return]
return self._resource(resource_id).get( # type: ignore[attr-defined, no-any-return]
query_params={"select": select},
options=self.query_state.options, # type: ignore[attr-defined]
)


class AsyncGetMixin[Model]:
Expand All @@ -32,4 +34,7 @@ async def get(self, resource_id: str, select: list[str] | str | None = None) ->
"""
if isinstance(select, list):
select = ",".join(select) if select else None
return await self._resource(resource_id).get(query_params={"select": select}) # type: ignore[attr-defined, no-any-return]
return await self._resource(resource_id).get( # type: ignore[attr-defined, no-any-return]
query_params={"select": select},
options=self.query_state.options, # type: ignore[attr-defined]
)
9 changes: 5 additions & 4 deletions mpt_api_client/http/mixins/queryable_mixin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Self

from mpt_api_client.http.query_options import QueryOptions
from mpt_api_client.http.query_state import QueryState
from mpt_api_client.rql import RQLQuery

Expand All @@ -23,7 +24,7 @@ def order_by(self, *fields: str) -> Self:
rql=self.query_state.filter, # type: ignore[attr-defined]
order_by=list(fields),
select=self.query_state.select, # type: ignore[attr-defined]
render=self.query_state.render, # type: ignore[attr-defined]
options=self.query_state.options, # type: ignore[attr-defined]
)
)

Expand All @@ -40,7 +41,7 @@ def filter(self, rql: RQLQuery) -> Self:
rql=combined_filter,
order_by=self.query_state.order_by, # type: ignore[attr-defined]
select=self.query_state.select, # type: ignore[attr-defined]
render=self.query_state.render, # type: ignore[attr-defined]
options=self.query_state.options, # type: ignore[attr-defined]
)
)

Expand All @@ -62,7 +63,7 @@ def select(self, *fields: str) -> Self:
rql=self.query_state.filter, # type: ignore[attr-defined]
order_by=self.query_state.order_by, # type: ignore[attr-defined]
select=list(fields),
render=self.query_state.render, # type: ignore[attr-defined]
options=self.query_state.options, # type: ignore[attr-defined]
),
)

Expand All @@ -77,7 +78,7 @@ def options(self, *, render: bool = False) -> Self:
rql=self.query_state.filter, # type: ignore[attr-defined]
order_by=self.query_state.order_by, # type: ignore[attr-defined]
select=self.query_state.select, # type: ignore[attr-defined]
render=render,
options=QueryOptions(render=render),
),
)

Expand Down
8 changes: 8 additions & 0 deletions mpt_api_client/http/query_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from dataclasses import dataclass


@dataclass
class QueryOptions:
"""Options for query state."""

render: bool = False
15 changes: 8 additions & 7 deletions mpt_api_client/http/query_state.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Any

from mpt_api_client.http.query_options import QueryOptions
from mpt_api_client.rql import RQLQuery


Expand All @@ -18,20 +19,20 @@ def __init__(
order_by: list[str] | None = None,
select: list[str] | None = None,
*,
render: bool = False,
options: QueryOptions | None = None,
) -> None:
"""Initialize the query state with optional filter, ordering, and selection criteria.

Args:
rql: RQL query for filtering data.
order_by: List of fields to order by (prefix with '-' for descending).
select: List of fields to select in the response.
render: Whether to include the render() parameter in the query string.
options: Query options for the request.
"""
self._filter = rql
self._order_by = order_by
self._select = select
self._render = render
self._options = options or QueryOptions()

@property
def filter(self) -> RQLQuery | None:
Expand All @@ -49,9 +50,9 @@ def select(self) -> list[str] | None:
return self._select

@property
def render(self) -> bool:
"""Get the current render state."""
return self._render
def options(self) -> QueryOptions:
"""Get the current query options."""
return self._options

def build(self, query_params: dict[str, Any] | None = None) -> str:
"""Build a query string from the current state and additional parameters.
Expand All @@ -75,7 +76,7 @@ def build(self, query_params: dict[str, Any] | None = None) -> str:
if self._filter:
query_parts.append(str(self._filter))

if self._render:
if self._options.render:
query_parts.append("render()")

if query_parts:
Expand Down
19 changes: 15 additions & 4 deletions mpt_api_client/http/resource_accessor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from mpt_api_client.constants import APPLICATION_JSON
from mpt_api_client.http.async_client import AsyncHTTPClient
from mpt_api_client.http.client import HTTPClient
from mpt_api_client.http.query_options import QueryOptions
from mpt_api_client.http.types import QueryParam, Response
from mpt_api_client.http.url_utils import join_url_path
from mpt_api_client.models.collection import ResourceList
Expand Down Expand Up @@ -37,6 +38,7 @@ def do_request( # noqa: WPS211
json: _JsonPayload = None,
query_params: QueryParam | None = None,
headers: dict[str, str] | None = None,
options: QueryOptions | None = None,
) -> Response:
"""Perform an HTTP request and return the raw ``Response``.

Expand All @@ -46,10 +48,11 @@ def do_request( # noqa: WPS211
json: JSON body payload.
query_params: Query-string parameters.
headers: Extra HTTP headers.
options: Query options.
"""
url = join_url_path(self._resource_url, action) if action else self._resource_url
return self._http_client.request(
method, url, json=json, query_params=query_params, headers=headers
method, url, json=json, query_params=query_params, headers=headers, options=options
)

# -- model-returning helpers ---------------------------------------------
Expand All @@ -59,9 +62,10 @@ def get(
action: str | None = None,
*,
query_params: QueryParam | None = None,
options: QueryOptions | None = None,
) -> ResourceModel:
"""``GET`` the resource (optionally with a sub-action)."""
return self._action("GET", action, query_params=query_params)
return self._action("GET", action, query_params=query_params, options=options)

def post(
self,
Expand Down Expand Up @@ -94,13 +98,15 @@ def _action(
*,
json: _JsonPayload = None,
query_params: QueryParam | None = None,
options: QueryOptions | None = None,
) -> ResourceModel:
response = self.do_request(
method,
action,
json=json,
query_params=query_params,
headers={"Accept": APPLICATION_JSON},
options=options,
)
return self._model_class.from_response(response)

Expand Down Expand Up @@ -131,6 +137,7 @@ async def do_request( # noqa: WPS211
json: _JsonPayload = None,
query_params: QueryParam | None = None,
headers: dict[str, str] | None = None,
options: QueryOptions | None = None,
) -> Response:
"""Perform an HTTP request and return the raw ``Response``.

Expand All @@ -140,10 +147,11 @@ async def do_request( # noqa: WPS211
json: JSON body payload.
query_params: Query-string parameters.
headers: Extra HTTP headers.
options: Additional options for the request.
"""
url = join_url_path(self._resource_url, action) if action else self._resource_url
return await self._http_client.request(
method, url, json=json, query_params=query_params, headers=headers
method, url, json=json, query_params=query_params, headers=headers, options=options
)

# -- model-returning helpers ---------------------------------------------
Expand All @@ -153,9 +161,10 @@ async def get(
action: str | None = None,
*,
query_params: QueryParam | None = None,
options: QueryOptions | None = None,
) -> ResourceModel:
"""``GET`` the resource (optionally with a sub-action)."""
return await self._action("GET", action, query_params=query_params)
return await self._action("GET", action, query_params=query_params, options=options)

async def post(
self,
Expand Down Expand Up @@ -188,12 +197,14 @@ async def _action(
*,
json: _JsonPayload = None,
query_params: QueryParam | None = None,
options: QueryOptions | None = None,
) -> ResourceModel:
response = await self.do_request(
method,
action,
json=json,
query_params=query_params,
headers={"Accept": APPLICATION_JSON},
options=options,
)
return self._model_class.from_response(response)
37 changes: 37 additions & 0 deletions tests/e2e/audit/records/test_async_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,40 @@ async def test_get_records_with_render(async_mpt_vendor: AsyncMPTClient, product
for record in records:
assert record.object.id == product_id
assert not any(char in record.details for char in template_chars)


async def test_get_record_with_render(
async_mpt_vendor: AsyncMPTClient, audit_record_id: str
) -> None:
template_chars = ["{{", "}}"]
service = async_mpt_vendor.audit.records.options(render=True)

result = await service.get(audit_record_id)

assert result.id == audit_record_id
assert not any(char in result.details for char in template_chars)


async def test_get_record_with_select(
async_mpt_vendor: AsyncMPTClient, audit_record_id: str
) -> None:
service = async_mpt_vendor.audit.records

result = await service.get(audit_record_id, select=["object", "actor"])

assert result.id == audit_record_id
assert result.object is not None
assert result.actor is not None


async def test_get_record_with_render_and_select(
async_mpt_vendor: AsyncMPTClient, audit_record_id: str
) -> None:
template_chars = ["{{", "}}"]
service = async_mpt_vendor.audit.records.options(render=True)

result = await service.get(audit_record_id, select=["object", "actor", "details"])

assert result.id == audit_record_id
assert result.object is not None
assert not any(char in result.details for char in template_chars)
Loading