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
1 change: 1 addition & 0 deletions mpt_api_client/http/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def __init__(
base_headers = {
"User-Agent": "swo-marketplace-client/1.0",
"Authorization": f"Bearer {api_token}",
"Accept": "application/json",
}
super().__init__(
base_url=base_url,
Expand Down
23 changes: 7 additions & 16 deletions mpt_api_client/http/async_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,6 @@ async def get(self, resource_id: str, select: list[str] | str | None = None) ->
select = ",".join(select) if select else None
return await self._resource_action(resource_id=resource_id, query_params={"select": select})

async def update(self, resource_id: str, resource_data: ResourceData) -> Model:
"""Update a resource using `PUT /endpoint/{resource_id}`.

Args:
resource_id: Resource ID.
resource_data: Resource data.

Returns:
Resource object.

"""
return await self._resource_action(resource_id, "PUT", json=resource_data)

async def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> httpx.Response:
"""Fetch one page of resources.

Expand All @@ -119,13 +106,14 @@ async def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> ht

return response

async def _resource_do_request(
async def _resource_do_request( # noqa: WPS211
self,
resource_id: str,
method: str = "GET",
action: str | None = None,
json: ResourceData | ResourceList | None = None,
query_params: QueryParam | None = None,
headers: dict[str, str] | None = None,
) -> httpx.Response:
"""Perform an action on a specific resource using.

Expand All @@ -138,13 +126,16 @@ async def _resource_do_request(
action: The action name to use.
json: The updated resource data.
query_params: Additional query parameters.
headers: Additional headers.

Raises:
HTTPError: If the action fails.
"""
resource_url = urljoin(f"{self._endpoint}/", resource_id)
resource_url = urljoin(f"{self.endpoint}/", resource_id)
url = urljoin(f"{resource_url}/", action) if action else resource_url
response = await self.http_client.request(method, url, json=json, params=query_params)
response = await self.http_client.request(
method, url, json=json, params=query_params, headers=headers
)
response.raise_for_status()
return response

Expand Down
39 changes: 26 additions & 13 deletions mpt_api_client/http/base_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from mpt_api_client.rql import RQLQuery


class ServiceBase[Client, Model: BaseModel]:
class ServiceBase[Client, Model: BaseModel]: # noqa: WPS214
"""Service base with agnostic HTTP client."""

_endpoint: str
Expand All @@ -22,11 +22,13 @@ def __init__(
query_rql: RQLQuery | None = None,
query_order_by: list[str] | None = None,
query_select: list[str] | None = None,
endpoint_params: dict[str, str] | None = None,
) -> None:
self.http_client = http_client
self.query_rql: RQLQuery | None = query_rql
self.query_order_by = query_order_by
self.query_select = query_select
self.endpoint_params = endpoint_params or {}

def clone(self) -> Self:
"""Create a copy of collection client for immutable operations.
Expand All @@ -39,30 +41,40 @@ def clone(self) -> Self:
query_rql=self.query_rql,
query_order_by=copy.copy(self.query_order_by) if self.query_order_by else None,
query_select=copy.copy(self.query_select) if self.query_select else None,
endpoint_params=self.endpoint_params,
)

def build_url(self, query_params: dict[str, Any] | None = None) -> str: # noqa: WPS210
@property
def endpoint(self) -> str:
"""Service endpoint URL."""
return self._endpoint.format(**self.endpoint_params)

def build_url(
self,
query_params: dict[str, Any] | None = None,
Copy link
Contributor

@svazquezco svazquezco Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove the comma to avoid splitting the code

) -> str: # noqa: WPS210
"""Builds the endpoint URL with all the query parameters.

Returns:
Partial URL with query parameters.
"""
query_params = query_params or {}
if self.query_order_by:
query_params.update({"order": ",".join(self.query_order_by)})
if self.query_select:
query_params.update({"select": ",".join(self.query_select)})

query_parts = [
f"{param_key}={param_value}" for param_key, param_value in query_params.items()
]
if self.query_order_by:
str_order_by = ",".join(self.query_order_by)
query_parts.append(f"order={str_order_by}")
if self.query_select:
str_query_select = ",".join(self.query_select)
query_parts.append(f"select={str_query_select}")

if self.query_rql:
query_parts.append(str(self.query_rql))

if query_parts:
query = "&".join(query_parts)
return f"{self._endpoint}?{query}"
return self._endpoint
return f"{self.endpoint}?{query}"
return self.endpoint

def order_by(self, *fields: str) -> Self:
"""Returns new collection with ordering setup.
Expand Down Expand Up @@ -109,12 +121,13 @@ def select(self, *fields: str) -> Self:
new_client.query_select = list(fields)
return new_client

def _create_collection(self, response: httpx.Response) -> Collection[Model]:
@classmethod
def _create_collection(cls, response: httpx.Response) -> Collection[Model]:
meta = Meta.from_response(response)
return Collection(
resources=[
self._model_class.new(resource, meta)
for resource in response.json().get(self._collection_key)
cls._model_class.new(resource, meta)
for resource in response.json().get(cls._collection_key)
],
meta=meta,
)
1 change: 1 addition & 0 deletions mpt_api_client/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def __init__(
base_headers = {
"User-Agent": "swo-marketplace-client/1.0",
"Authorization": f"Bearer {api_token}",
"content-type": "application/json",
}
super().__init__(
base_url=base_url,
Expand Down
40 changes: 37 additions & 3 deletions mpt_api_client/http/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def create(self, resource_data: ResourceData) -> Model:
Returns:
New resource created.
"""
response = self.http_client.post(self._endpoint, json=resource_data) # type: ignore[attr-defined]
response = self.http_client.post(self.endpoint, json=resource_data) # type: ignore[attr-defined]
response.raise_for_status()

return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]
Expand All @@ -31,6 +31,23 @@ def delete(self, resource_id: str) -> None:
response.raise_for_status()


class UpdateMixin[Model]:
"""Update resource mixin."""

def update(self, resource_id: str, resource_data: ResourceData) -> Model:
"""Update a resource using `PUT /endpoint/{resource_id}`.

Args:
resource_id: Resource ID.
resource_data: Resource data.

Returns:
Resource object.

"""
return self._resource_action(resource_id, "PUT", json=resource_data) # type: ignore[attr-defined, no-any-return]


class AsyncCreateMixin[Model]:
"""Create resource mixin."""

Expand All @@ -40,7 +57,7 @@ async def create(self, resource_data: ResourceData) -> Model:
Returns:
New resource created.
"""
response = await self.http_client.post(self._endpoint, json=resource_data) # type: ignore[attr-defined]
response = await self.http_client.post(self.endpoint, json=resource_data) # type: ignore[attr-defined]
response.raise_for_status()

return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]
Expand All @@ -55,6 +72,23 @@ async def delete(self, resource_id: str) -> None:
Args:
resource_id: Resource ID.
"""
url = urljoin(f"{self._endpoint}/", resource_id) # type: ignore[attr-defined]
url = urljoin(f"{self.endpoint}/", resource_id) # type: ignore[attr-defined]
response = await self.http_client.delete(url) # type: ignore[attr-defined]
response.raise_for_status()


class AsyncUpdateMixin[Model]:
"""Update resource mixin."""

async def update(self, resource_id: str, resource_data: ResourceData) -> Model:
"""Update a resource using `PUT /endpoint/{resource_id}`.

Args:
resource_id: Resource ID.
resource_data: Resource data.

Returns:
Resource object.

"""
return await self._resource_action(resource_id, "PUT", json=resource_data) # type: ignore[attr-defined, no-any-return]
32 changes: 14 additions & 18 deletions mpt_api_client/http/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from mpt_api_client.models.collection import ResourceList


class Service[Model: BaseModel](ServiceBase[HTTPClient, Model]):
class Service[Model: BaseModel](ServiceBase[HTTPClient, Model]): # noqa: WPS214
"""Immutable service for RESTful resource collections.

Examples:
Expand Down Expand Up @@ -91,19 +91,6 @@ def get(self, resource_id: str, select: list[str] | str | None = None) -> Model:

return self._resource_action(resource_id=resource_id, query_params={"select": select})

def update(self, resource_id: str, resource_data: ResourceData) -> Model:
"""Update a resource using `PUT /endpoint/{resource_id}`.

Args:
resource_id: Resource ID.
resource_data: Resource data.

Returns:
Resource object.

"""
return self._resource_action(resource_id, "PUT", json=resource_data)

def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> httpx.Response:
"""Fetch one page of resources.

Expand All @@ -119,13 +106,14 @@ def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> httpx.Re

return response

def _resource_do_request(
def _resource_do_request( # noqa: WPS211
self,
resource_id: str,
method: str = "GET",
action: str | None = None,
json: ResourceData | ResourceList | None = None,
query_params: QueryParam | None = None,
headers: dict[str, str] | None = None,
) -> httpx.Response:
"""Perform an action on a specific resource using `HTTP_METHOD /endpoint/{resource_id}`.

Expand All @@ -135,16 +123,19 @@ def _resource_do_request(
action: The action name to use.
json: The updated resource data.
query_params: Additional query parameters.
headers: Additional headers.

Returns:
HTTP response object.

Raises:
HTTPError: If the action fails.
"""
resource_url = urljoin(f"{self._endpoint}/", resource_id)
resource_url = urljoin(f"{self.endpoint}/", resource_id)
url = urljoin(f"{resource_url}/", action) if action else resource_url
response = self.http_client.request(method, url, json=json, params=query_params)
response = self.http_client.request(
method, url, json=json, params=query_params, headers=headers
)
response.raise_for_status()
return response

Expand All @@ -166,6 +157,11 @@ def _resource_action(
query_params: Additional query parameters.
"""
response = self._resource_do_request(
resource_id, method, action, json=json, query_params=query_params
resource_id,
method,
action,
json=json,
query_params=query_params,
headers={"Accept": "application/json"},
)
return self._model_class.from_response(response)
3 changes: 2 additions & 1 deletion mpt_api_client/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from mpt_api_client.models.collection import Collection
from mpt_api_client.models.file_model import FileModel
from mpt_api_client.models.meta import Meta, Pagination
from mpt_api_client.models.model import Model, ResourceData

__all__ = ["Collection", "Meta", "Model", "Pagination", "ResourceData"] # noqa: WPS410
__all__ = ["Collection", "FileModel", "Meta", "Model", "Pagination", "ResourceData"] # noqa: WPS410
55 changes: 55 additions & 0 deletions mpt_api_client/models/file_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import re

from httpx import Response


class FileModel:
"""File resource."""

def __init__(self, response: Response):
self.response = response

@property
def filename(self) -> str | None:
"""Filename from Content-Disposition header.

Returns:
The filename if found in the Content-Disposition header, None otherwise.
"""
content_disposition = self.response.headers.get("content-disposition")
if not content_disposition:
return None

filename_match = re.search(
r'filename\*=(?:UTF-8\'\')?([^;]+)|filename=(?:"([^"]+)"|([^;]+))',
content_disposition,
re.IGNORECASE,
)

if filename_match:
return filename_match.group(1) or filename_match.group(2) or filename_match.group(3)

return None

@property
def file_contents(self) -> bytes:
"""Returns the content of the attachment.

Returns:
The content of the attachment in bytes

Raises:
ResponseNotRead()

"""
return self.response.content

@property
def content_type(self) -> str | None:
"""Returns the content type of the attachment.

Returns:
The content type of the attachment.
"""
ctype = self.response.headers.get("content-type", "")
return str(ctype)
Loading