Skip to content

Commit 6716030

Browse files
authored
Merge pull request #1197 from sdelgadoc/master
Add additional functionality to subscriptions
2 parents 33cef7e + 9135cc2 commit 6716030

File tree

2 files changed

+116
-8
lines changed

2 files changed

+116
-8
lines changed

O365/subscriptions.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import datetime as dt
22
from typing import Iterable, Mapping, Optional, Union
33

4-
from .utils import ApiComponent
4+
from .utils import ApiComponent, NEXT_LINK_KEYWORD, Pagination
55

66

77
class Subscriptions(ApiComponent):
@@ -76,6 +76,27 @@ def _stringify_change_type(change_type: Union[str, Iterable[str]]) -> str:
7676
raise ValueError("change_type must contain at least one value.")
7777
return value
7878

79+
def get_subscription(
80+
self,
81+
subscription_id: str,
82+
*,
83+
params: Optional[Mapping[str, object]] = None,
84+
**request_kwargs,
85+
) -> Optional[dict]:
86+
"""Retrieve a single webhook subscription by id."""
87+
if not subscription_id:
88+
raise ValueError("subscription_id must be provided.")
89+
if params is not None and not isinstance(params, Mapping):
90+
raise ValueError("params must be a mapping if provided.")
91+
92+
url = self._build_subscription_url(subscription_id)
93+
response = self.con.get(url, params=params, **request_kwargs)
94+
95+
if not response:
96+
return None
97+
98+
return response.json()
99+
79100
def create_subscription(
80101
self,
81102
notification_url: str,
@@ -146,6 +167,38 @@ def create_subscription(
146167

147168
return response.json()
148169

170+
def list_subscriptions(
171+
self,
172+
*,
173+
limit: Optional[int] = None,
174+
**request_kwargs,
175+
) -> Union[Iterable[dict], Pagination]:
176+
"""List webhook subscriptions visible to the current app/context."""
177+
if limit is not None and limit <= 0:
178+
raise ValueError("limit must be a positive integer.")
179+
180+
url = self._build_subscription_url()
181+
response = self.con.get(url, **request_kwargs)
182+
if not response:
183+
return iter(())
184+
185+
data = response.json()
186+
subscriptions = data.get("value", [])
187+
next_link = data.get(NEXT_LINK_KEYWORD)
188+
189+
if next_link:
190+
return Pagination(
191+
parent=self,
192+
data=subscriptions,
193+
next_link=next_link,
194+
limit=limit,
195+
)
196+
197+
if limit is not None:
198+
return subscriptions[:limit]
199+
200+
return subscriptions
201+
149202
def renew_subscription(
150203
self,
151204
subscription_id: str,
@@ -175,6 +228,43 @@ def renew_subscription(
175228

176229
return response.json()
177230

231+
def update_subscription(
232+
self,
233+
subscription_id: str,
234+
*,
235+
notification_url: Optional[str] = None,
236+
expiration_datetime: Optional[dt.datetime] = None,
237+
expiration_minutes: Optional[int] = None,
238+
**request_kwargs,
239+
) -> Optional[dict]:
240+
"""Update subscription fields (expiration and/or notification URL)."""
241+
if not subscription_id:
242+
raise ValueError("subscription_id must be provided.")
243+
244+
payload = {}
245+
246+
if expiration_datetime is not None or expiration_minutes is not None:
247+
payload[self._cc("expiration_date_time")] = self._format_subscription_expiration(
248+
expiration_datetime=expiration_datetime,
249+
expiration_minutes=expiration_minutes,
250+
)
251+
252+
if notification_url is not None:
253+
if not notification_url:
254+
raise ValueError("notification_url, if provided, cannot be empty.")
255+
payload[self._cc("notification_url")] = notification_url
256+
257+
if not payload:
258+
raise ValueError("At least one of expiration or notification_url must be provided.")
259+
260+
url = self._build_subscription_url(subscription_id)
261+
response = self.con.patch(url, data=payload, **request_kwargs)
262+
263+
if not response:
264+
return None
265+
266+
return response.json()
267+
178268
def delete_subscription(
179269
self,
180270
subscription_id: str,

examples/subscriptions_example.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@
33
44
Quickstart for this example:
55
1) Run Flask locally withg the following command:
6-
- flask --app examples/subscription_account_webhook.py run --debug
6+
- flask --app examples/subscriptions_example.py run --debug
77
2) Expose HTTPS via a tunnel to your localhost:5000:
88
- Free: pinggy (https://pinggy.io/) to get https://<subdomain>.pinggy.link -> http://localhost:5000
99
- Paid/free-tier: ngrok (https://ngrok.com/): ngrok http 5000, note the https URL.
1010
3) Use the tunnel HTTPS URL as notification_url pointing to /webhook, URL-encoded.
1111
4) To create a subscription, follow the example request below:
1212
- https://<your-tunnel-host>/subscriptions?notification_url=https%3A%2F%2F<your-tunnel-host>%2Fwebhook&client_state=abc123
13-
4) To renew a subscription, follow the example request below:
13+
5) To list subscriptions, follow the example request below:
14+
- http://<your-tunnel-host>/subscriptions/list
15+
6) To renew a subscription, follow the example request below:
1416
- http://<your-tunnel-host>/subscriptions/<subscription_id>/renew?expiration_minutes=55
15-
5) To delete a subscription, follow the example request below:
17+
7) To delete a subscription, follow the example request below:
1618
- http://<your-tunnel-host>/subscriptions/<subscription_id>/delete
1719
Graph will call https://<your-tunnel-host>/webhook; this app echoes validationToken and returns 202 for notifications.
1820
"""
@@ -38,7 +40,7 @@
3840
])
3941

4042
RESOURCE = "/me/mailFolders('inbox')/messages"
41-
DEFAULT_EXPIRATION_MINUTES = 55 # Graph requires renewals before the limit
43+
DEFAULT_EXPIRATION_MINUTES = 10069 # Maximum expiration is 10,070 in the future.
4244

4345
app = Flask(__name__)
4446

@@ -63,7 +65,7 @@ def create_subscription():
6365
client_state = request.args.get("client_state")
6466
resource = request.args.get("resource", RESOURCE)
6567

66-
subscription = account.subscriptions.create_subscription(
68+
subscription = account.subscriptions().create_subscription(
6769
notification_url=notification_url,
6870
resource=resource,
6971
change_type="created",
@@ -73,10 +75,26 @@ def create_subscription():
7375
return jsonify(subscription), 201
7476

7577

78+
@app.get("/subscriptions/list")
79+
def list_subscriptions():
80+
limit_raw = request.args.get("limit")
81+
limit = None
82+
if limit_raw is not None:
83+
try:
84+
limit = int(limit_raw)
85+
except ValueError:
86+
abort(400, description="limit must be an integer")
87+
if limit <= 0:
88+
abort(400, description="limit must be a positive integer")
89+
90+
subscriptions = account.subscriptions().list_subscriptions(limit=limit)
91+
return jsonify(list(subscriptions)), 200
92+
93+
7694
@app.get("/subscriptions/<subscription_id>/renew")
7795
def renew_subscription(subscription_id: str):
7896
expiration_minutes = _int_arg("expiration_minutes", DEFAULT_EXPIRATION_MINUTES)
79-
updated = account.subscriptions.renew_subscription(
97+
updated = account.subscriptions().renew_subscription(
8098
subscription_id,
8199
expiration_minutes=expiration_minutes,
82100
)
@@ -85,7 +103,7 @@ def renew_subscription(subscription_id: str):
85103

86104
@app.get("/subscriptions/<subscription_id>/delete")
87105
def delete_subscription(subscription_id: str):
88-
deleted = account.subscriptions.delete_subscription(subscription_id)
106+
deleted = account.subscriptions().delete_subscription(subscription_id)
89107
if not deleted:
90108
abort(404, description="Subscription not found")
91109
return ("", 204)

0 commit comments

Comments
 (0)