Skip to content

Commit 4887b5f

Browse files
authored
Merge pull request #386
MPT-24265 add notifications directories, footers and webhooks services
2 parents 816f5b0 + cd32da0 commit 4887b5f

23 files changed

Lines changed: 1031 additions & 0 deletions

‎e2e_config.test.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"notifications.batch.id": "MST-3638-2460-4825",
6969
"notifications.category.id": "NTC-6157-0397",
7070
"notifications.contact.id": "CTT-0001-9158",
71+
"notifications.directory.id": "DIR-9614-2985",
7172
"notifications.message.id": "MSG-0000-6215-1019-0139",
7273
"notifications.subscriber.id": "NTS-0829-7123-7123",
7374
"integration.extension.id": "EXT-6587-4477",
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from mpt_api_client.http import AsyncService, Service
2+
from mpt_api_client.http.mixins import (
3+
AsyncCollectionMixin,
4+
AsyncGetMixin,
5+
CollectionMixin,
6+
GetMixin,
7+
)
8+
from mpt_api_client.models import Model
9+
from mpt_api_client.models.model import BaseModel
10+
11+
12+
class Directory(Model):
13+
"""Notifications Directory resource.
14+
15+
Attributes:
16+
account: Reference to the account owning the directory.
17+
origin: Reference to the account the directory originates from.
18+
type: Directory type.
19+
contacts: References to the contacts belonging to the directory.
20+
audit: Audit information (created, updated events).
21+
"""
22+
23+
account: BaseModel | None
24+
origin: BaseModel | None
25+
type: str | None
26+
contacts: list[BaseModel] | None
27+
audit: BaseModel | None
28+
29+
30+
class DirectoriesServiceConfig:
31+
"""Notifications Directories service configuration."""
32+
33+
_endpoint = "/public/v1/notifications/directories"
34+
_model_class = Directory
35+
_collection_key = "data"
36+
37+
38+
class DirectoriesService(
39+
GetMixin[Directory],
40+
CollectionMixin[Directory],
41+
Service[Directory],
42+
DirectoriesServiceConfig,
43+
):
44+
"""Notifications Directories service."""
45+
46+
47+
class AsyncDirectoriesService(
48+
AsyncGetMixin[Directory],
49+
AsyncCollectionMixin[Directory],
50+
AsyncService[Directory],
51+
DirectoriesServiceConfig,
52+
):
53+
"""Async Notifications Directories service."""
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from mpt_api_client.http import AsyncService, Service
2+
from mpt_api_client.http.mixins import (
3+
AsyncCollectionMixin,
4+
AsyncManagedResourceMixin,
5+
CollectionMixin,
6+
ManagedResourceMixin,
7+
)
8+
from mpt_api_client.models import Model
9+
from mpt_api_client.models.model import BaseModel
10+
11+
12+
class Footer(Model):
13+
"""Notifications Footer resource.
14+
15+
Attributes:
16+
language_code: Language code of the footer.
17+
content: Content of the footer.
18+
is_default: Whether this footer is the default one.
19+
status: Footer status.
20+
audit: Audit information (created, updated events).
21+
"""
22+
23+
language_code: str | None
24+
content: str | None
25+
is_default: bool | None
26+
status: str | None
27+
audit: BaseModel | None
28+
29+
30+
class FootersServiceConfig:
31+
"""Notifications Footers service configuration."""
32+
33+
_endpoint = "/public/v1/notifications/footers"
34+
_model_class = Footer
35+
_collection_key = "data"
36+
37+
38+
class FootersService(
39+
ManagedResourceMixin[Footer],
40+
CollectionMixin[Footer],
41+
Service[Footer],
42+
FootersServiceConfig,
43+
):
44+
"""Notifications Footers service."""
45+
46+
47+
class AsyncFootersService(
48+
AsyncManagedResourceMixin[Footer],
49+
AsyncCollectionMixin[Footer],
50+
AsyncService[Footer],
51+
FootersServiceConfig,
52+
):
53+
"""Async Notifications Footers service."""

‎mpt_api_client/resources/notifications/notifications.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,20 @@
66
CategoriesService,
77
)
88
from mpt_api_client.resources.notifications.contacts import AsyncContactsService, ContactsService
9+
from mpt_api_client.resources.notifications.directories import (
10+
AsyncDirectoriesService,
11+
DirectoriesService,
12+
)
13+
from mpt_api_client.resources.notifications.footers import AsyncFootersService, FootersService
914
from mpt_api_client.resources.notifications.messages import AsyncMessagesService, MessagesService
1015
from mpt_api_client.resources.notifications.subscribers import (
1116
AsyncSubscribersService,
1217
SubscribersService,
1318
)
19+
from mpt_api_client.resources.notifications.webhooks import (
20+
AsyncWebhooksService,
21+
WebhooksService,
22+
)
1423

1524

1625
class Notifications:
@@ -51,6 +60,16 @@ def contacts(self) -> ContactsService:
5160
"""Contacts service."""
5261
return ContactsService(http_client=self.http_client)
5362

63+
@property
64+
def directories(self) -> DirectoriesService:
65+
"""Directories service."""
66+
return DirectoriesService(http_client=self.http_client)
67+
68+
@property
69+
def footers(self) -> FootersService:
70+
"""Footers service."""
71+
return FootersService(http_client=self.http_client)
72+
5473
@property
5574
def messages(self) -> MessagesService:
5675
"""Messages service."""
@@ -61,6 +80,11 @@ def subscribers(self) -> SubscribersService:
6180
"""Subscriptions service."""
6281
return SubscribersService(http_client=self.http_client)
6382

83+
@property
84+
def webhooks(self) -> WebhooksService:
85+
"""Webhooks service."""
86+
return WebhooksService(http_client=self.http_client)
87+
6488

6589
class AsyncNotifications:
6690
"""Notifications MPT API Module."""
@@ -100,6 +124,16 @@ def contacts(self) -> AsyncContactsService:
100124
"""Async Contacts service."""
101125
return AsyncContactsService(http_client=self.http_client)
102126

127+
@property
128+
def directories(self) -> AsyncDirectoriesService:
129+
"""Async Directories service."""
130+
return AsyncDirectoriesService(http_client=self.http_client)
131+
132+
@property
133+
def footers(self) -> AsyncFootersService:
134+
"""Async Footers service."""
135+
return AsyncFootersService(http_client=self.http_client)
136+
103137
@property
104138
def messages(self) -> AsyncMessagesService:
105139
"""Async Messages service."""
@@ -109,3 +143,8 @@ def messages(self) -> AsyncMessagesService:
109143
def subscribers(self) -> AsyncSubscribersService:
110144
"""Subscriptions service."""
111145
return AsyncSubscribersService(http_client=self.http_client)
146+
147+
@property
148+
def webhooks(self) -> AsyncWebhooksService:
149+
"""Async Webhooks service."""
150+
return AsyncWebhooksService(http_client=self.http_client)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from mpt_api_client.http import AsyncService, Service
2+
from mpt_api_client.http.mixins import (
3+
AsyncCollectionMixin,
4+
AsyncDisableMixin,
5+
AsyncEnableMixin,
6+
AsyncManagedResourceMixin,
7+
CollectionMixin,
8+
DisableMixin,
9+
EnableMixin,
10+
ManagedResourceMixin,
11+
)
12+
from mpt_api_client.models import Model
13+
from mpt_api_client.models.model import BaseModel
14+
15+
16+
class Webhook(Model):
17+
"""Notifications Webhook resource.
18+
19+
Attributes:
20+
name: Webhook name.
21+
url: Endpoint the webhook calls.
22+
description: Webhook description.
23+
status: Webhook status.
24+
type: Webhook type.
25+
secret: Shared secret used to sign webhook calls.
26+
statistics: Call statistics of the webhook.
27+
object_type: Type of the object the webhook is bound to.
28+
account: Reference to the account owning the webhook.
29+
object: Reference to the object the webhook is bound to.
30+
criteria: Key/value criteria narrowing when the webhook fires.
31+
last_success: Last successful call.
32+
last_failure: Last failed call.
33+
last_call: Last call regardless of its outcome.
34+
audit: Audit information (created, updated events).
35+
"""
36+
37+
name: str | None
38+
url: str | None
39+
description: str | None
40+
status: str | None
41+
type: str | None
42+
secret: str | None
43+
statistics: BaseModel | None
44+
object_type: str | None
45+
account: BaseModel | None
46+
object: BaseModel | None
47+
criteria: list[BaseModel] | None
48+
last_success: BaseModel | None
49+
last_failure: BaseModel | None
50+
last_call: BaseModel | None
51+
audit: BaseModel | None
52+
53+
54+
class WebhooksServiceConfig:
55+
"""Notifications Webhooks service configuration."""
56+
57+
_endpoint = "/public/v1/notifications/webhooks"
58+
_model_class = Webhook
59+
_collection_key = "data"
60+
61+
62+
class WebhooksService(
63+
EnableMixin[Webhook],
64+
DisableMixin[Webhook],
65+
ManagedResourceMixin[Webhook],
66+
CollectionMixin[Webhook],
67+
Service[Webhook],
68+
WebhooksServiceConfig,
69+
):
70+
"""Notifications Webhooks service."""
71+
72+
73+
class AsyncWebhooksService(
74+
AsyncEnableMixin[Webhook],
75+
AsyncDisableMixin[Webhook],
76+
AsyncManagedResourceMixin[Webhook],
77+
AsyncCollectionMixin[Webhook],
78+
AsyncService[Webhook],
79+
WebhooksServiceConfig,
80+
):
81+
"""Async Notifications Webhooks service."""

‎pyproject.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ per-file-ignores = [
133133
"mpt_api_client/resources/commerce/*.py: WPS235 WPS215",
134134
"mpt_api_client/resources/exchange/*.py: WPS235 WPS215",
135135
"mpt_api_client/resources/integration/*.py: WPS204 WPS214 WPS215 WPS235",
136+
"mpt_api_client/resources/notifications/footers.py: WPS110 WPS215",
137+
"mpt_api_client/resources/notifications/notifications.py: WPS214",
136138
"mpt_api_client/resources/helpdesk/*.py: WPS204 WPS215 WPS214",
137139
"mpt_api_client/resources/program/*.py: WPS204 WPS215 WPS235",
138140
"mpt_api_client/rql/query_builder.py: WPS110 WPS115 WPS210 WPS214",

‎tests/e2e/helper.py‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,62 @@ def create_fixture_resource_and_delete(service, resource_data, upload_file=None)
5858
_delete_resource(service, resource)
5959

6060

61+
async def _async_create_first_free(service, candidates, is_taken):
62+
remaining = list(candidates)
63+
while remaining:
64+
candidate = remaining.pop(0)
65+
try:
66+
return await service.create(candidate)
67+
except MPTAPIError as error:
68+
if not is_taken(error):
69+
raise
70+
raise RuntimeError(f"Every candidate payload is already taken: {candidates}")
71+
72+
73+
def _create_first_free(service, candidates, is_taken):
74+
for candidate in candidates:
75+
try:
76+
return service.create(candidate)
77+
except MPTAPIError as error:
78+
if not is_taken(error):
79+
raise
80+
raise RuntimeError(f"Every candidate payload is already taken: {candidates}")
81+
82+
83+
@asynccontextmanager
84+
async def async_create_first_free_fixture_and_delete(service, candidates, is_taken):
85+
"""Create the first candidate the platform accepts, then delete it on teardown.
86+
87+
``candidates`` are payloads that differ only in a server-enforced uniqueness key and
88+
``is_taken(error)`` tells whether a rejected create means that key is already in use.
89+
The create call itself is the claim, so two runs that race for the same key cannot both
90+
succeed and the loser moves on to the next candidate instead of failing.
91+
"""
92+
resource = await _async_create_first_free(service, candidates, is_taken)
93+
94+
try:
95+
yield resource
96+
finally:
97+
await _delete_async_resource(service, resource)
98+
99+
100+
@contextmanager
101+
def create_first_free_fixture_and_delete(service, candidates, is_taken):
102+
"""Create the first candidate the platform accepts, then delete it on teardown.
103+
104+
``candidates`` are payloads that differ only in a server-enforced uniqueness key and
105+
``is_taken(error)`` tells whether a rejected create means that key is already in use.
106+
The create call itself is the claim, so two runs that race for the same key cannot both
107+
succeed and the loser moves on to the next candidate instead of failing.
108+
"""
109+
resource = _create_first_free(service, candidates, is_taken)
110+
111+
try:
112+
yield resource
113+
finally:
114+
_delete_resource(service, resource)
115+
116+
61117
@asynccontextmanager
62118
async def async_create_fixture_resource_and_finalize(service, resource_data, finalize, logger):
63119
"""Create a resource, then transition it to a terminal state on teardown.

‎tests/e2e/notifications/directories/__init__.py‎

Whitespace-only changes.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import pytest
2+
3+
4+
@pytest.fixture
5+
def directories_service(mpt_ops):
6+
return mpt_ops.notifications.directories
7+
8+
9+
@pytest.fixture
10+
def async_directories_service(async_mpt_ops):
11+
return async_mpt_ops.notifications.directories
12+
13+
14+
@pytest.fixture(scope="session")
15+
def directory_id(e2e_config):
16+
return e2e_config["notifications.directory.id"]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import pytest
2+
3+
from mpt_api_client.exceptions import MPTAPIError
4+
from tests.e2e.helper import assert_async_service_filter_with_iterate
5+
6+
pytestmark = [pytest.mark.flaky]
7+
8+
9+
async def test_get_directory(async_directories_service, directory_id):
10+
result = await async_directories_service.get(directory_id)
11+
12+
assert result.id == directory_id
13+
14+
15+
async def test_get_directory_not_found(async_directories_service):
16+
bogus_id = "DIR-0000-0000"
17+
18+
with pytest.raises(MPTAPIError, match=r"404 Not Found"):
19+
await async_directories_service.get(bogus_id)
20+
21+
22+
async def test_filter_directories(async_directories_service, directory_id):
23+
await assert_async_service_filter_with_iterate(
24+
async_directories_service, directory_id, None
25+
) # act

0 commit comments

Comments
 (0)