Skip to content

Commit cdeedb1

Browse files
committed
MPT-14532 Cleanup base service
1 parent a61214a commit cdeedb1

38 files changed

+449
-215
lines changed

mpt_api_client/http/async_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ async def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> Re
8585
HTTPStatusError: if the response status code is not 200.
8686
"""
8787
pagination_params: dict[str, int] = {"limit": limit, "offset": offset}
88-
return await self.http_client.request("get", self.build_url(pagination_params))
88+
return await self.http_client.request("get", self.build_path(pagination_params))
8989

9090
async def _resource_do_request( # noqa: WPS211
9191
self,
@@ -112,7 +112,7 @@ async def _resource_do_request( # noqa: WPS211
112112
Raises:
113113
HTTPError: If the action fails.
114114
"""
115-
resource_url = urljoin(f"{self.endpoint}/", resource_id)
115+
resource_url = urljoin(f"{self.path}/", resource_id)
116116
url = urljoin(f"{resource_url}/", action) if action else resource_url
117117
return await self.http_client.request(
118118
method, url, json=json, query_params=query_params, headers=headers

mpt_api_client/http/base_service.py

Lines changed: 40 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import copy
21
from typing import Any, Self
32

3+
from mpt_api_client.http.query_state import QueryState
44
from mpt_api_client.http.types import Response
55
from mpt_api_client.models import Collection, Meta
66
from mpt_api_client.models import Model as BaseModel
@@ -18,62 +18,29 @@ def __init__(
1818
self,
1919
*,
2020
http_client: Client,
21-
query_rql: RQLQuery | None = None,
22-
query_order_by: list[str] | None = None,
23-
query_select: list[str] | None = None,
21+
query_state: QueryState | None = None,
2422
endpoint_params: dict[str, str] | None = None,
2523
) -> None:
2624
self.http_client = http_client
27-
self.query_rql: RQLQuery | None = query_rql
28-
self.query_order_by = query_order_by
29-
self.query_select = query_select
25+
self.query_state = query_state or QueryState()
3026
self.endpoint_params = endpoint_params or {}
3127

32-
def clone(self) -> Self:
33-
"""Create a copy of collection client for immutable operations.
34-
35-
Returns:
36-
New collection client with same settings.
37-
"""
38-
return type(self)(
39-
http_client=self.http_client,
40-
query_rql=self.query_rql,
41-
query_order_by=copy.copy(self.query_order_by) if self.query_order_by else None,
42-
query_select=copy.copy(self.query_select) if self.query_select else None,
43-
endpoint_params=self.endpoint_params,
44-
)
45-
4628
@property
47-
def endpoint(self) -> str:
29+
def path(self) -> str:
4830
"""Service endpoint URL."""
4931
return self._endpoint.format(**self.endpoint_params)
5032

51-
def build_url(
33+
def build_path(
5234
self,
5335
query_params: dict[str, Any] | None = None,
54-
) -> str: # noqa: WPS210
36+
) -> str:
5537
"""Builds the endpoint URL with all the query parameters.
5638
5739
Returns:
58-
Partial URL with query parameters.
40+
Complete URL with query parameters.
5941
"""
60-
query_params = query_params or {}
61-
if self.query_order_by:
62-
query_params.update({"order": ",".join(self.query_order_by)})
63-
if self.query_select:
64-
query_params.update({"select": ",".join(self.query_select)})
65-
66-
query_parts = [
67-
f"{param_key}={param_value}" for param_key, param_value in query_params.items()
68-
]
69-
70-
if self.query_rql:
71-
query_parts.append(str(self.query_rql))
72-
73-
if query_parts:
74-
query = "&".join(query_parts)
75-
return f"{self.endpoint}?{query}"
76-
return self.endpoint
42+
query = self.query_state.build(query_params)
43+
return f"{self.path}?{query}" if query else self.path
7744

7845
def order_by(self, *fields: str) -> Self:
7946
"""Returns new collection with ordering setup.
@@ -84,23 +51,35 @@ def order_by(self, *fields: str) -> Self:
8451
Raises:
8552
ValueError: If ordering has already been set.
8653
"""
87-
if self.query_order_by is not None:
54+
if self.query_state.order_by is not None:
8855
raise ValueError("Ordering is already set. Cannot set ordering multiple times.")
89-
new_collection = self.clone()
90-
new_collection.query_order_by = list(fields)
91-
return new_collection
56+
return type(self)(
57+
http_client=self.http_client,
58+
query_state=QueryState(
59+
rql=self.query_state.filter,
60+
order_by=list(fields),
61+
select=self.query_state.select,
62+
),
63+
endpoint_params=self.endpoint_params,
64+
)
9265

9366
def filter(self, rql: RQLQuery) -> Self:
9467
"""Creates a new collection with the filter added to the filter collection.
9568
9669
Returns:
9770
New copy of the collection with the filter added.
9871
"""
99-
if self.query_rql:
100-
rql = self.query_rql & rql
101-
new_collection = self.clone()
102-
new_collection.query_rql = rql
103-
return new_collection
72+
existing_filter = self.query_state.filter
73+
combined_filter = existing_filter & rql if existing_filter else rql
74+
return type(self)(
75+
http_client=self.http_client,
76+
query_state=QueryState(
77+
rql=combined_filter,
78+
order_by=self.query_state.order_by,
79+
select=self.query_state.select,
80+
),
81+
endpoint_params=self.endpoint_params,
82+
)
10483

10584
def select(self, *fields: str) -> Self:
10685
"""Set select fields. Raises ValueError if select fields are already set.
@@ -111,14 +90,19 @@ def select(self, *fields: str) -> Self:
11190
Raises:
11291
ValueError: If select fields are already set.
11392
"""
114-
if self.query_select is not None:
93+
if self.query_state.select is not None:
11594
raise ValueError(
11695
"Select fields are already set. Cannot set select fields multiple times."
11796
)
118-
119-
new_client = self.clone()
120-
new_client.query_select = list(fields)
121-
return new_client
97+
return type(self)(
98+
http_client=self.http_client,
99+
query_state=QueryState(
100+
rql=self.query_state.filter,
101+
order_by=self.query_state.order_by,
102+
select=list(fields),
103+
),
104+
endpoint_params=self.endpoint_params,
105+
)
122106

123107
@classmethod
124108
def _create_collection(cls, response: Response) -> Collection[Model]:

mpt_api_client/http/mixins.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def create(self, resource_data: ResourceData) -> Model:
2020
Returns:
2121
New resource created.
2222
"""
23-
response = self.http_client.request("post", self.endpoint, json=resource_data) # type: ignore[attr-defined]
23+
response = self.http_client.request("post", self.path, json=resource_data) # type: ignore[attr-defined]
2424

2525
return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]
2626

@@ -82,7 +82,7 @@ def create(
8282
"application/json",
8383
)
8484

85-
response = self.http_client.request("post", self.endpoint, files=files) # type: ignore[attr-defined]
85+
response = self.http_client.request("post", self.path, files=files) # type: ignore[attr-defined]
8686

8787
return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]
8888

@@ -110,7 +110,7 @@ async def create(self, resource_data: ResourceData) -> Model:
110110
Returns:
111111
New resource created.
112112
"""
113-
response = await self.http_client.request("post", self.endpoint, json=resource_data) # type: ignore[attr-defined]
113+
response = await self.http_client.request("post", self.path, json=resource_data) # type: ignore[attr-defined]
114114

115115
return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]
116116

@@ -124,7 +124,7 @@ async def delete(self, resource_id: str) -> None:
124124
Args:
125125
resource_id: Resource ID.
126126
"""
127-
url = urljoin(f"{self.endpoint}/", resource_id) # type: ignore[attr-defined]
127+
url = urljoin(f"{self.path}/", resource_id) # type: ignore[attr-defined]
128128
await self.http_client.request("delete", url) # type: ignore[attr-defined]
129129

130130

@@ -173,7 +173,7 @@ async def create(
173173
"application/json",
174174
)
175175

176-
response = await self.http_client.request("post", self.endpoint, files=files) # type: ignore[attr-defined]
176+
response = await self.http_client.request("post", self.path, files=files) # type: ignore[attr-defined]
177177

178178
return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]
179179

mpt_api_client/http/query_state.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import copy
2+
from typing import Any, Self
3+
4+
from mpt_api_client.rql import RQLQuery
5+
6+
7+
class QueryState:
8+
"""Stores and manages API query state for filtering, selecting, and ordering data.
9+
10+
This class maintains the current state of query parameters (filter, order_by, select)
11+
and provides functionality to build query strings from that state. It's responsible
12+
for both storing query configuration and constructing the appropriate query parameters
13+
that modify the behavior and shape of data returned by the API.
14+
"""
15+
16+
def __init__(
17+
self,
18+
rql: RQLQuery | None = None,
19+
order_by: list[str] | None = None,
20+
select: list[str] | None = None,
21+
) -> None:
22+
"""Initialize the query state with optional filter, ordering, and selection criteria.
23+
24+
Args:
25+
rql: RQL query for filtering data.
26+
order_by: List of fields to order by (prefix with '-' for descending).
27+
select: List of fields to select in the response.
28+
"""
29+
self._filter = rql
30+
self._order_by = order_by
31+
self._select = select
32+
33+
@property
34+
def filter(self) -> RQLQuery | None:
35+
"""Get the current filter query."""
36+
return self._filter
37+
38+
@property
39+
def order_by(self) -> list[str] | None:
40+
"""Get the current order by fields."""
41+
return self._order_by
42+
43+
@property
44+
def select(self) -> list[str] | None:
45+
"""Get the current select fields."""
46+
return self._select
47+
48+
def clone(self) -> Self:
49+
"""Create a copy of the query state for immutable operations.
50+
51+
Returns:
52+
New query state instance with the same filter, ordering, and selection settings.
53+
"""
54+
return type(self)(
55+
rql=self._filter,
56+
order_by=copy.copy(self._order_by) if self._order_by else None,
57+
select=copy.copy(self._select) if self._select else None,
58+
)
59+
60+
def build(self, query_params: dict[str, Any] | None = None) -> str:
61+
"""Build a query string from the current state and additional parameters.
62+
63+
Args:
64+
query_params: Additional query parameters to include in the query string.
65+
66+
Returns:
67+
Complete query string with all parameters, or empty string if no parameters.
68+
"""
69+
query_params = query_params or {}
70+
if self._order_by:
71+
query_params.update({"order": ",".join(self._order_by)})
72+
if self._select:
73+
query_params.update({"select": ",".join(self._select)})
74+
75+
query_parts = [
76+
f"{param_key}={param_value}" for param_key, param_value in query_params.items()
77+
]
78+
79+
if self._filter:
80+
query_parts.append(str(self._filter))
81+
82+
if query_parts:
83+
query = "&".join(query_parts)
84+
return f"{query}"
85+
return ""

mpt_api_client/http/service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> Response
8484
HTTPStatusError: if the response status code is not 200.
8585
"""
8686
pagination_params: dict[str, int] = {"limit": limit, "offset": offset}
87-
return self.http_client.request("get", self.build_url(pagination_params))
87+
return self.http_client.request("get", self.build_path(pagination_params))
8888

8989
def _resource_do_request( # noqa: WPS211
9090
self,
@@ -111,7 +111,7 @@ def _resource_do_request( # noqa: WPS211
111111
Raises:
112112
HTTPError: If the action fails.
113113
"""
114-
resource_url = urljoin(f"{self.endpoint}/", resource_id)
114+
resource_url = urljoin(f"{self.path}/", resource_id)
115115
url = urljoin(f"{resource_url}/", action) if action else resource_url
116116
return self.http_client.request(
117117
method, url, json=json, query_params=query_params, headers=headers

mpt_api_client/resources/notifications/batches.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def create(
4949
"application/json",
5050
)
5151

52-
response = self.http_client.request("post", self.endpoint, files=files)
52+
response = self.http_client.request("post", self.path, files=files)
5353
return self._model_class.from_response(response)
5454

5555
def get_batch_attachment(self, batch_id: str, attachment_id: str) -> FileModel:
@@ -63,7 +63,7 @@ def get_batch_attachment(self, batch_id: str, attachment_id: str) -> FileModel:
6363
FileModel containing the attachment.
6464
"""
6565
response = self.http_client.request(
66-
"get", f"{self.endpoint}/{batch_id}/attachments/{attachment_id}"
66+
"get", f"{self.path}/{batch_id}/attachments/{attachment_id}"
6767
)
6868

6969
return FileModel(response)
@@ -101,7 +101,7 @@ async def create(
101101
"application/json",
102102
)
103103

104-
response = await self.http_client.request("post", self.endpoint, files=files)
104+
response = await self.http_client.request("post", self.path, files=files)
105105
return self._model_class.from_response(response)
106106

107107
async def get_batch_attachment(self, batch_id: str, attachment_id: str) -> FileModel:
@@ -115,6 +115,6 @@ async def get_batch_attachment(self, batch_id: str, attachment_id: str) -> FileM
115115
FileModel containing the attachment.
116116
"""
117117
response = await self.http_client.request(
118-
"get", f"{self.endpoint}/{batch_id}/attachments/{attachment_id}"
118+
"get", f"{self.path}/{batch_id}/attachments/{attachment_id}"
119119
)
120120
return FileModel(response)

tests/http/test_async_service.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import respx
44

55
from mpt_api_client.exceptions import MPTAPIError
6+
from mpt_api_client.http.query_state import QueryState
67
from tests.http.conftest import AsyncDummyService
78

89

@@ -96,22 +97,22 @@ async def test_async_fetch_page_with_filter(
9697

9798

9899
def test_async_init_defaults(async_dummy_service):
99-
assert async_dummy_service.query_rql is None
100-
assert async_dummy_service.query_order_by is None
101-
assert async_dummy_service.query_select is None
102-
assert async_dummy_service.build_url() == "/api/v1/test"
100+
assert async_dummy_service.query_state.filter is None
101+
assert async_dummy_service.query_state.order_by is None
102+
assert async_dummy_service.query_state.select is None
103+
assert async_dummy_service.build_path() == "/api/v1/test"
103104

104105

105106
def test_async_init_with_filter(async_http_client, filter_status_active):
106107
collection_client = AsyncDummyService(
107108
http_client=async_http_client,
108-
query_rql=filter_status_active,
109+
query_state=QueryState(rql=filter_status_active),
109110
)
110111

111-
assert collection_client.query_rql == filter_status_active
112-
assert collection_client.query_order_by is None
113-
assert collection_client.query_select is None
114-
assert collection_client.build_url() == "/api/v1/test?eq(status,active)"
112+
assert collection_client.query_state.filter == filter_status_active
113+
assert collection_client.query_state.order_by is None
114+
assert collection_client.query_state.select is None
115+
assert collection_client.build_path() == "/api/v1/test?eq(status,active)"
115116

116117

117118
async def test_async_iterate_single_page(async_dummy_service, single_page_response):

0 commit comments

Comments
 (0)