Skip to content

Commit f680314

Browse files
jentykclaude
andcommitted
feat(notifications): add directories, footers and webhooks services
The /public/v1/notifications/directories, /public/v1/notifications/footers and /public/v1/notifications/webhooks collection endpoints had no service class, so they were unreachable from MPTClient and AsyncMPTClient. Add sync and async services for all three and register them as properties on the Notifications group. Mixins follow what the OpenAPI spec documents per endpoint: directories are read-only (GET collection and GET by id), footers are fully managed, and webhooks are managed with enable/disable actions. Adding any service to Notifications pushes the group class past wemake-python-styleguide's max_methods default of 7, which it was sitting exactly on, so mpt_api_client/resources/notifications/*.py needs a per-file-ignores entry for WPS214, as every other group at 8+ methods already has. The entry also repeats WPS215 (previously inherited from the broader mpt_api_client/resources/* pattern, which a more specific entry replaces rather than extends) and adds WPS110 for the API-mandated Footer.content field name. Streaming support for these endpoints is out of scope and tracked separately in MPT-24241. MPT-24265 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 816f5b0 commit f680314

9 files changed

Lines changed: 560 additions & 0 deletions

File tree

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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ 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+
# notifications: footers.py carries the API-mandated `content` field (WPS110) and, like every
137+
# service module, stacks more than three mixins (WPS215, restated because this entry shadows
138+
# the resources/* one); notifications.py exposes more than seven services (WPS214).
139+
"mpt_api_client/resources/notifications/footers.py: WPS110 WPS215",
140+
"mpt_api_client/resources/notifications/notifications.py: WPS214",
136141
"mpt_api_client/resources/helpdesk/*.py: WPS204 WPS215 WPS214",
137142
"mpt_api_client/resources/program/*.py: WPS204 WPS215 WPS235",
138143
"mpt_api_client/rql/query_builder.py: WPS110 WPS115 WPS210 WPS214",
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import httpx
2+
import pytest
3+
import respx
4+
5+
from mpt_api_client.resources.notifications.directories import (
6+
AsyncDirectoriesService,
7+
DirectoriesService,
8+
)
9+
10+
11+
@pytest.fixture
12+
def directories_service(http_client):
13+
return DirectoriesService(http_client=http_client)
14+
15+
16+
@pytest.fixture
17+
def async_directories_service(async_http_client):
18+
return AsyncDirectoriesService(http_client=async_http_client)
19+
20+
21+
@pytest.fixture
22+
def directory_data():
23+
return {
24+
"id": "NDR-1234",
25+
"type": "Vendor",
26+
"account": {"id": "ACC-1234", "name": "Account"},
27+
"origin": {"id": "ACC-5678", "name": "Origin account"},
28+
"contacts": [{"id": "CON-1234", "email": "contact@example.com"}],
29+
"audit": {"created": {"at": "2024-01-01T00:00:00Z"}},
30+
}
31+
32+
33+
def test_endpoint(directories_service):
34+
result = directories_service.build_path()
35+
36+
assert result == "/public/v1/notifications/directories"
37+
38+
39+
def test_async_endpoint(async_directories_service):
40+
result = async_directories_service.build_path()
41+
42+
assert result == "/public/v1/notifications/directories"
43+
44+
45+
@pytest.mark.parametrize("method", ["get", "iterate", "fetch_page"])
46+
def test_mixins_present(directories_service, method):
47+
result = hasattr(directories_service, method)
48+
49+
assert result is True
50+
51+
52+
@pytest.mark.parametrize("method", ["get", "iterate", "fetch_page"])
53+
def test_async_mixins_present(async_directories_service, method):
54+
result = hasattr(async_directories_service, method)
55+
56+
assert result is True
57+
58+
59+
@pytest.mark.parametrize("method", ["create", "update", "delete"])
60+
def test_write_mixins_absent(directories_service, method):
61+
result = hasattr(directories_service, method)
62+
63+
assert result is False
64+
65+
66+
@pytest.mark.parametrize("method", ["create", "update", "delete"])
67+
def test_async_write_mixins_absent(async_directories_service, method):
68+
result = hasattr(async_directories_service, method)
69+
70+
assert result is False
71+
72+
73+
def test_get_directory(directories_service, directory_data):
74+
with respx.mock:
75+
mock_route = respx.get(
76+
"https://api.example.com/public/v1/notifications/directories/NDR-1234"
77+
).mock(
78+
return_value=httpx.Response(
79+
status_code=200,
80+
headers={"content-type": "application/json"},
81+
json=directory_data,
82+
)
83+
)
84+
85+
result = directories_service.get("NDR-1234")
86+
87+
assert mock_route.call_count == 1
88+
assert result.to_dict() == directory_data
89+
assert result.account.id == "ACC-1234"
90+
assert result.contacts[0].id == "CON-1234"

0 commit comments

Comments
 (0)