Skip to content

Commit 0dad396

Browse files
committed
MPT-19660 Improve resource action implementation
1 parent a2b1bbd commit 0dad396

5 files changed

Lines changed: 220 additions & 108 deletions

File tree

mpt_api_client/http/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from mpt_api_client.http.async_client import AsyncHTTPClient
22
from mpt_api_client.http.async_service import AsyncService
33
from mpt_api_client.http.client import HTTPClient
4+
from mpt_api_client.http.resource import async_resource_action, resource_action
45
from mpt_api_client.http.service import Service
56

67
__all__ = [ # noqa: WPS410
78
"AsyncHTTPClient",
89
"AsyncService",
910
"HTTPClient",
1011
"Service",
12+
"async_resource_action",
13+
"resource_action",
1114
]
Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,18 @@
1+
from mpt_api_client.http.resource import async_resource_action, resource_action
12
from mpt_api_client.models import ResourceData
23

34

45
class UpdateMixin[Model]:
56
"""Update resource mixin."""
67

7-
def update(self, resource_id: str, resource_data: ResourceData) -> Model:
8-
"""Update a resource using `PUT /endpoint/{resource_id}`.
9-
10-
Args:
11-
resource_id: Resource ID.
12-
resource_data: Resource data.
13-
14-
Returns:
15-
Resource object.
16-
17-
"""
18-
return self._resource_action(resource_id, "PUT", json=resource_data) # type: ignore[attr-defined, no-any-return]
8+
@resource_action(None, method="PUT")
9+
def update(self, resource_id: str, resource_data: ResourceData) -> Model: # type: ignore[empty-body]
10+
"""Update a resource using `PUT /endpoint/{resource_id}`."""
1911

2012

2113
class AsyncUpdateMixin[Model]:
2214
"""Update resource mixin."""
2315

24-
async def update(self, resource_id: str, resource_data: ResourceData) -> Model:
25-
"""Update a resource using `PUT /endpoint/{resource_id}`.
26-
27-
Args:
28-
resource_id: Resource ID.
29-
resource_data: Resource data.
30-
31-
Returns:
32-
Resource object.
33-
34-
"""
35-
return await self._resource_action(resource_id, "PUT", json=resource_data) # type: ignore[attr-defined, no-any-return]
16+
@async_resource_action(None, method="PUT")
17+
async def update(self, resource_id: str, resource_data: ResourceData) -> Model: # type: ignore[empty-body]
18+
"""Update a resource using `PUT /endpoint/{resource_id}`."""

mpt_api_client/http/resource.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
"""Declarative resource action decorators.
2+
3+
Reduce boilerplate when defining REST resource actions by decorating method
4+
stubs instead of writing the ``_resource_action(…)`` call by hand.
5+
6+
Example::
7+
8+
class ActivatableMixin[Model]:
9+
@resource_action
10+
def activate(
11+
self, resource_id: str, resource_data: ResourceData | None = None,
12+
) -> Model: ...
13+
14+
class AsyncActivatableMixin[Model]:
15+
@async_resource_action
16+
async def activate(
17+
self, resource_id: str, resource_data: ResourceData | None = None,
18+
) -> Model: ...
19+
20+
The action name defaults to the method name. Override it (or pass ``None``
21+
for no sub-path) with ``@resource_action("custom-name")`` or
22+
``@resource_action(None, method="PUT")``.
23+
"""
24+
25+
from __future__ import annotations
26+
27+
import functools
28+
from collections.abc import Callable
29+
from typing import Any, overload
30+
31+
from mpt_api_client.models import ResourceData
32+
33+
_SENTINEL = object()
34+
35+
36+
@overload
37+
def resource_action[Fn: Callable[..., Any]](fn: Fn, /) -> Fn: ...
38+
@overload
39+
def resource_action(
40+
action: str | None = ..., # noqa: WPS125
41+
/,
42+
*,
43+
method: str = ...,
44+
) -> Callable[..., Any]: ...
45+
46+
47+
def resource_action(
48+
fn_or_action: Any = _SENTINEL,
49+
/,
50+
*,
51+
method: str = "POST",
52+
) -> Any:
53+
"""Decorator that wires a **sync** method stub to ``_resource_action``.
54+
55+
When used without arguments the URL action name is taken from the
56+
method name::
57+
58+
@resource_action
59+
def activate(self, resource_id, resource_data=None) -> Model: ...
60+
61+
Pass a string to override the action name, or ``None`` for endpoints
62+
without a sub-path::
63+
64+
@resource_action("sso-check")
65+
def sso_check(self, resource_id, resource_data=None) -> Model: ...
66+
67+
@resource_action(None, method="PUT")
68+
def update(self, resource_id, resource_data=None) -> Model: ...
69+
"""
70+
if callable(fn_or_action):
71+
fn = fn_or_action
72+
act: str | None = fn.__name__
73+
74+
@functools.wraps(fn)
75+
def wrapper(
76+
self: Any,
77+
resource_id: str,
78+
resource_data: ResourceData | None = None,
79+
) -> Any:
80+
return self._resource_action(
81+
resource_id,
82+
method,
83+
act,
84+
json=resource_data,
85+
)
86+
87+
return wrapper
88+
89+
act_raw = fn_or_action
90+
91+
def decorator[Fn: Callable[..., Any]](fn: Fn) -> Fn:
92+
action_name: str | None = fn.__name__ if act_raw is _SENTINEL else act_raw
93+
94+
@functools.wraps(fn)
95+
def wrapper(
96+
self: Any,
97+
resource_id: str,
98+
resource_data: ResourceData | None = None,
99+
) -> Any:
100+
return self._resource_action(
101+
resource_id,
102+
method,
103+
action_name,
104+
json=resource_data,
105+
)
106+
107+
return wrapper # type: ignore[return-value]
108+
109+
return decorator
110+
111+
112+
@overload
113+
def async_resource_action[Fn: Callable[..., Any]](fn: Fn, /) -> Fn: ...
114+
@overload
115+
def async_resource_action(
116+
action: str | None = ..., # noqa: WPS125
117+
/,
118+
*,
119+
method: str = ...,
120+
) -> Callable[..., Any]: ...
121+
122+
123+
def async_resource_action(
124+
fn_or_action: Any = _SENTINEL,
125+
/,
126+
*,
127+
method: str = "POST",
128+
) -> Any:
129+
"""Decorator that wires an **async** method stub to ``_resource_action``.
130+
131+
Async counterpart of :func:`resource_action`. Usage is identical but
132+
the decorated method must be ``async def``::
133+
134+
@async_resource_action
135+
async def activate(self, resource_id, resource_data=None) -> Model: ...
136+
"""
137+
if callable(fn_or_action):
138+
fn = fn_or_action
139+
act: str | None = fn.__name__
140+
141+
@functools.wraps(fn)
142+
async def wrapper(
143+
self: Any,
144+
resource_id: str,
145+
resource_data: ResourceData | None = None,
146+
) -> Any:
147+
return await self._resource_action(
148+
resource_id,
149+
method,
150+
act,
151+
json=resource_data,
152+
)
153+
154+
return wrapper
155+
156+
act_raw = fn_or_action
157+
158+
def decorator[Fn: Callable[..., Any]](fn: Fn) -> Fn:
159+
action_name: str | None = fn.__name__ if act_raw is _SENTINEL else act_raw
160+
161+
@functools.wraps(fn)
162+
async def wrapper(
163+
self: Any,
164+
resource_id: str,
165+
resource_data: ResourceData | None = None,
166+
) -> Any:
167+
return await self._resource_action(
168+
resource_id,
169+
method,
170+
action_name,
171+
json=resource_data,
172+
)
173+
174+
return wrapper # type: ignore[return-value]
175+
176+
return decorator
Lines changed: 18 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,30 @@
1+
from mpt_api_client.http.resource import async_resource_action, resource_action
12
from mpt_api_client.models import ResourceData
23

34

45
class ActivatableMixin[Model]:
5-
"""Activatable mixin for activating, enabling, disabling and deactivating resources."""
6+
"""Activatable mixin for activating and deactivating resources."""
67

7-
def activate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
8-
"""Activate a resource.
8+
@resource_action
9+
def activate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: # type: ignore[empty-body]
10+
"""Activate a resource."""
911

10-
Args:
11-
resource_id: Resource ID
12-
resource_data: Resource data will be updated
13-
"""
14-
return self._resource_action( # type: ignore[attr-defined, no-any-return]
15-
resource_id, "POST", "activate", json=resource_data
16-
)
17-
18-
def deactivate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
19-
"""Deactivate a resource.
20-
21-
Args:
22-
resource_id: Resource ID
23-
resource_data: Resource data will be updated
24-
"""
25-
return self._resource_action( # type: ignore[attr-defined, no-any-return]
26-
resource_id, "POST", "deactivate", json=resource_data
27-
)
12+
@resource_action
13+
def deactivate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: # type: ignore[empty-body]
14+
"""Deactivate a resource."""
2815

2916

3017
class AsyncActivatableMixin[Model]:
31-
"""Async activatable mixin for activating, enabling, disabling and deactivating resources."""
32-
33-
async def activate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
34-
"""Activate a resource.
18+
"""Async activatable mixin for activating and deactivating resources."""
3519

36-
Args:
37-
resource_id: Resource ID
38-
resource_data: Resource data will be updated
39-
"""
40-
return await self._resource_action( # type: ignore[attr-defined, no-any-return]
41-
resource_id, "POST", "activate", json=resource_data
42-
)
20+
@async_resource_action
21+
async def activate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: # type: ignore[empty-body]
22+
"""Activate a resource."""
4323

44-
async def deactivate(
45-
self, resource_id: str, resource_data: ResourceData | None = None
24+
@async_resource_action
25+
async def deactivate( # type: ignore[empty-body]
26+
self,
27+
resource_id: str,
28+
resource_data: ResourceData | None = None,
4629
) -> Model:
47-
"""Deactivate a resource.
48-
49-
Args:
50-
resource_id: Resource ID
51-
resource_data: Resource data will be updated
52-
"""
53-
return await self._resource_action( # type: ignore[attr-defined, no-any-return]
54-
resource_id, "POST", "deactivate", json=resource_data
55-
)
30+
"""Deactivate a resource."""
Lines changed: 16 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,30 @@
1+
from mpt_api_client.http.resource import async_resource_action, resource_action
12
from mpt_api_client.models import ResourceData
23

34

45
class ActivatableMixin[Model]:
56
"""Activatable mixin adds the ability to activate and deactivate."""
67

7-
def activate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
8-
"""Update state to Active.
8+
@resource_action
9+
def activate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: # type: ignore[empty-body]
10+
"""Update state to Active."""
911

10-
Args:
11-
resource_id: Resource ID
12-
resource_data: Resource data will be updated
13-
"""
14-
return self._resource_action( # type: ignore[attr-defined, no-any-return]
15-
resource_id, "POST", "activate", json=resource_data
16-
)
17-
18-
def deactivate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
19-
"""Update state to Inactive.
20-
21-
Args:
22-
resource_id: Resource ID
23-
resource_data: Resource data will be updated
24-
"""
25-
return self._resource_action( # type: ignore[attr-defined, no-any-return]
26-
resource_id, "POST", "deactivate", json=resource_data
27-
)
12+
@resource_action
13+
def deactivate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: # type: ignore[empty-body]
14+
"""Update state to Inactive."""
2815

2916

3017
class AsyncActivatableMixin[Model]:
3118
"""Activatable mixin adds the ability to activate and deactivate."""
3219

33-
async def activate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
34-
"""Update state to Active.
35-
36-
Args:
37-
resource_id: Resource ID
38-
resource_data: Resource data will be updated
39-
"""
40-
return await self._resource_action( # type: ignore[attr-defined, no-any-return]
41-
resource_id, "POST", "activate", json=resource_data
42-
)
20+
@async_resource_action
21+
async def activate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: # type: ignore[empty-body]
22+
"""Update state to Active."""
4323

44-
async def deactivate(
45-
self, resource_id: str, resource_data: ResourceData | None = None
24+
@async_resource_action
25+
async def deactivate( # type: ignore[empty-body]
26+
self,
27+
resource_id: str,
28+
resource_data: ResourceData | None = None,
4629
) -> Model:
47-
"""Update state to Inactive.
48-
49-
Args:
50-
resource_id: Resource ID
51-
resource_data: Resource data will be updated
52-
"""
53-
return await self._resource_action( # type: ignore[attr-defined, no-any-return]
54-
resource_id, "POST", "deactivate", json=resource_data
55-
)
30+
"""Update state to Inactive."""

0 commit comments

Comments
 (0)