2025-01-16 08:38:51 -08:00
|
|
|
|
import json
|
2024-06-18 10:06:57 -07:00
|
|
|
|
from datetime import datetime, timezone
|
2022-08-05 00:25:03 -07:00
|
|
|
|
|
2025-06-05 08:48:21 -07:00
|
|
|
|
from flask import current_app
|
|
|
|
|
|
|
2020-05-27 11:56:07 +01:00
|
|
|
|
from app.extensions import redis_client
|
2022-09-09 17:02:48 -07:00
|
|
|
|
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
|
2015-12-04 14:40:16 +00:00
|
|
|
|
|
|
|
|
|
|
|
2016-11-30 17:01:44 +00:00
|
|
|
|
class ServiceAPIClient(NotifyAdminAPIClient):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("user-{user_id}")
|
2017-10-04 11:49:32 +01:00
|
|
|
|
def create_service(
|
|
|
|
|
|
self,
|
|
|
|
|
|
service_name,
|
2023-07-12 12:09:44 -04:00
|
|
|
|
organization_type,
|
2017-10-04 11:49:32 +01:00
|
|
|
|
message_limit,
|
|
|
|
|
|
restricted,
|
|
|
|
|
|
user_id,
|
|
|
|
|
|
email_from,
|
|
|
|
|
|
):
|
2016-01-14 14:55:07 +00:00
|
|
|
|
"""
|
|
|
|
|
|
Create a service and return the json.
|
|
|
|
|
|
"""
|
|
|
|
|
|
data = {
|
|
|
|
|
|
"name": service_name,
|
2023-07-12 12:09:44 -04:00
|
|
|
|
"organization_type": organization_type,
|
2016-11-16 11:44:29 +00:00
|
|
|
|
"active": True,
|
2016-04-12 14:19:51 +01:00
|
|
|
|
"message_limit": message_limit,
|
2016-02-19 16:38:08 +00:00
|
|
|
|
"user_id": user_id,
|
2016-03-31 15:17:05 +01:00
|
|
|
|
"restricted": restricted,
|
2019-02-12 14:02:21 +00:00
|
|
|
|
"email_from": email_from,
|
2016-01-14 14:55:07 +00:00
|
|
|
|
}
|
2016-08-11 14:20:43 +01:00
|
|
|
|
data = _attach_current_user(data)
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.post("/service", data)["data"]["id"]
|
2016-01-14 14:55:07 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.set("service-{service_id}")
|
2016-07-20 11:46:29 +01:00
|
|
|
|
def get_service(self, service_id):
|
2016-01-19 10:23:08 +00:00
|
|
|
|
"""
|
|
|
|
|
|
Retrieve a service.
|
|
|
|
|
|
"""
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.get("/service/{0}".format(service_id))
|
2016-07-25 15:26:43 +01:00
|
|
|
|
|
2021-06-28 10:11:22 +01:00
|
|
|
|
def get_service_statistics(self, service_id, limit_days=None):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.get(
|
|
|
|
|
|
"/service/{0}/statistics".format(service_id),
|
|
|
|
|
|
params={"limit_days": limit_days},
|
|
|
|
|
|
)["data"]
|
2016-01-19 10:23:08 +00:00
|
|
|
|
|
2024-06-03 15:34:43 -07:00
|
|
|
|
def get_service_notification_statistics_by_day(
|
|
|
|
|
|
self, service_id, start_date=None, days=None
|
|
|
|
|
|
):
|
2024-05-29 12:13:09 -07:00
|
|
|
|
if start_date is None:
|
2024-06-03 15:34:43 -07:00
|
|
|
|
start_date = datetime.now().strftime("%Y-%m-%d")
|
2024-05-29 12:13:09 -07:00
|
|
|
|
|
|
|
|
|
|
return self.get(
|
|
|
|
|
|
"/service/{0}/statistics/{1}/{2}".format(service_id, start_date, days),
|
|
|
|
|
|
)["data"]
|
|
|
|
|
|
|
2024-06-12 14:32:25 -07:00
|
|
|
|
def get_user_service_notification_statistics_by_day(
|
|
|
|
|
|
self, service_id, user_id, start_date=None, days=None
|
|
|
|
|
|
):
|
|
|
|
|
|
if start_date is None:
|
2024-06-18 10:06:57 -07:00
|
|
|
|
start_date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
2024-06-12 14:32:25 -07:00
|
|
|
|
|
|
|
|
|
|
return self.get(
|
2024-06-17 12:36:21 -07:00
|
|
|
|
"/service/{0}/statistics/user/{1}/{2}/{3}".format(
|
|
|
|
|
|
service_id, user_id, start_date, days
|
|
|
|
|
|
),
|
2024-06-12 14:32:25 -07:00
|
|
|
|
)["data"]
|
|
|
|
|
|
|
2016-11-09 14:33:35 +00:00
|
|
|
|
def get_services(self, params_dict=None):
|
2016-01-19 10:23:08 +00:00
|
|
|
|
"""
|
|
|
|
|
|
Retrieve a list of services.
|
|
|
|
|
|
"""
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.get("/service", params=params_dict)
|
2016-11-09 14:33:35 +00:00
|
|
|
|
|
2019-08-13 16:25:26 +01:00
|
|
|
|
def find_services_by_name(self, service_name):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.get(
|
|
|
|
|
|
"/service/find-services-by-name", params={"service_name": service_name}
|
|
|
|
|
|
)
|
2019-08-13 16:25:26 +01:00
|
|
|
|
|
2019-04-25 18:07:40 +01:00
|
|
|
|
def get_live_services_data(self, params_dict=None):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Retrieve a list of live services data with contact names and notification counts.
|
|
|
|
|
|
"""
|
2025-06-05 08:48:21 -07:00
|
|
|
|
try:
|
|
|
|
|
|
return self.get("/service/live-services-data", params=params_dict)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
current_app.logger.error(
|
|
|
|
|
|
f"Error getting live services data {str(e)}", exc_info=True
|
|
|
|
|
|
)
|
|
|
|
|
|
raise e
|
2019-04-25 18:07:40 +01:00
|
|
|
|
|
2016-11-09 14:33:35 +00:00
|
|
|
|
def get_active_services(self, params_dict=None):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Retrieve a list of active services.
|
|
|
|
|
|
"""
|
2023-08-25 09:12:23 -07:00
|
|
|
|
params_dict["only_active"] = True
|
2016-11-09 14:33:35 +00:00
|
|
|
|
return self.get_services(params_dict)
|
2016-01-19 10:23:08 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
|
|
|
|
|
def update_service(self, service_id, **kwargs):
|
2016-01-14 14:55:07 +00:00
|
|
|
|
"""
|
|
|
|
|
|
Update a service.
|
|
|
|
|
|
"""
|
2016-08-11 14:20:43 +01:00
|
|
|
|
data = _attach_current_user(kwargs)
|
|
|
|
|
|
disallowed_attributes = set(data.keys()) - {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"active",
|
|
|
|
|
|
"billing_contact_email_addresses",
|
|
|
|
|
|
"billing_contact_names",
|
|
|
|
|
|
"billing_reference",
|
|
|
|
|
|
"consent_to_research",
|
|
|
|
|
|
"contact_link",
|
|
|
|
|
|
"created_by",
|
|
|
|
|
|
"count_as_live",
|
|
|
|
|
|
"email_from",
|
|
|
|
|
|
"free_sms_fragment_limit",
|
|
|
|
|
|
"go_live_at",
|
|
|
|
|
|
"go_live_user",
|
|
|
|
|
|
"message_limit",
|
|
|
|
|
|
"name",
|
|
|
|
|
|
"notes",
|
|
|
|
|
|
"organization_type",
|
|
|
|
|
|
"permissions",
|
|
|
|
|
|
"prefix_sms",
|
|
|
|
|
|
"purchase_order_number",
|
|
|
|
|
|
"rate_limit",
|
|
|
|
|
|
"reply_to_email_address",
|
|
|
|
|
|
"research_mode",
|
|
|
|
|
|
"restricted",
|
|
|
|
|
|
"sms_sender",
|
|
|
|
|
|
"volume_email",
|
|
|
|
|
|
"volume_sms",
|
2016-08-11 13:55:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
if disallowed_attributes:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
raise TypeError(
|
|
|
|
|
|
"Not allowed to update service attributes: {}".format(
|
|
|
|
|
|
", ".join(disallowed_attributes)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2021-01-25 14:03:16 +00:00
|
|
|
|
|
2016-01-14 14:55:07 +00:00
|
|
|
|
endpoint = "/service/{0}".format(service_id)
|
2016-03-08 10:40:07 +00:00
|
|
|
|
return self.post(endpoint, data)
|
2016-01-14 14:55:07 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("live-service-and-organization-counts")
|
2019-04-10 17:20:51 +01:00
|
|
|
|
def update_status(self, service_id, live):
|
2025-06-05 08:48:21 -07:00
|
|
|
|
try:
|
|
|
|
|
|
return self.update_service(
|
|
|
|
|
|
service_id,
|
|
|
|
|
|
message_limit=250000 if live else 50,
|
|
|
|
|
|
restricted=(not live),
|
|
|
|
|
|
go_live_at=str(datetime.utcnow()) if live else None,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
current_app.logger.error(f"Error updating status {str(e)}", exc_info=True)
|
|
|
|
|
|
raise e
|
2019-04-10 17:20:51 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("live-service-and-organization-counts")
|
2019-07-08 14:32:18 +01:00
|
|
|
|
def update_count_as_live(self, service_id, count_as_live):
|
2025-06-05 08:48:21 -07:00
|
|
|
|
try:
|
|
|
|
|
|
return self.update_service(
|
|
|
|
|
|
service_id,
|
|
|
|
|
|
count_as_live=count_as_live,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
current_app.logger.error(
|
|
|
|
|
|
f"Error updating count as live {str(e)}", exc_info=True
|
|
|
|
|
|
)
|
|
|
|
|
|
raise e
|
2019-07-08 14:32:18 +01:00
|
|
|
|
|
Add Redis cache between admin and API
Most of the time spent by the admin app to generate a page is spent
waiting for the API. This is slow for three reasons:
1. Talking to the API means going out to the internet, then through
nginx, the Flask app, SQLAlchemy, down to the database, and then
serialising the result to JSON and making it into a HTTP response
2. Each call to the API is synchronous, therefore if a page needs 3 API
calls to render then the second API call won’t be made until the
first has finished, and the third won’t start until the second has
finished
3. Every request for a service page in the admin app makes a minimum
of two requests to the API (`GET /service/…` and `GET /user/…`)
Hitting the database will always be the slowest part of an app like
Notify. But this slowness is exacerbated by 2. and 3. Conversely every
speedup made to 1. is multiplied by 2. and 3.
So this pull request aims to make 1. a _lot_ faster by taking nginx,
Flask, SQLAlchemy and the database out of the equation. It replaces them
with Redis, which as an in-memory key/value store is a lot faster than
Postgres. There is still the overhead of going across the network to
talk to Redis, but the net improvement is vast.
This commit only caches the `GET /service` response, but is written in
such a way that we can easily expand to caching other responses down the
line.
The tradeoff here is that our code is more complex, and we risk
introducing edge cases where a cache becomes stale. The mitigations
against this are:
- invalidating all caches after 24h so a stale cache doesn’t remain
around indefinitely
- being careful when we add new stuff to the service response
---
Some indicative numbers, based on:
- `GET http://localhost:6012/services/<service_id>/template/<template_id>`
- with the admin app running locally
- talking to Redis running locally
- also talking to the API running locally, itself talking to a local
Postgres instance
- times measured with Chrome web inspector, average of 10 requests
╲ | No cache | Cache service | Cache service and user | Cache service, user and template
-- | -- | -- | -- | --
**Request time** | 136ms | 97ms | 73ms | 37ms
**Improvement** | 0% | 41% | 88% | 265%
---
Estimates of how much storage this requires:
- Services: 1,942 on production × 2kb = 4Mb
- Users: 4,534 on production × 2kb = 9Mb
- Templates: 7,079 on production × 4kb = 28Mb
2018-04-06 13:37:49 +01:00
|
|
|
|
# This method is not cached because it calls through to one which is
|
2016-06-01 16:07:43 +01:00
|
|
|
|
def update_service_with_properties(self, service_id, properties):
|
2016-08-11 12:32:38 +01:00
|
|
|
|
return self.update_service(service_id, **properties)
|
2016-06-01 16:07:43 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
|
|
|
|
|
@cache.delete("service-{service_id}-templates")
|
|
|
|
|
|
@cache.delete_by_pattern("service-{service_id}-template-*")
|
2020-05-22 17:12:00 +01:00
|
|
|
|
def archive_service(self, service_id, cached_service_user_ids):
|
2022-09-08 09:07:54 -07:00
|
|
|
|
if cached_service_user_ids:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
redis_client.delete(*map("user-{}".format, cached_service_user_ids))
|
|
|
|
|
|
return self.post("/service/{}/archive".format(service_id), data=None)
|
2016-11-02 16:53:40 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
2017-01-31 15:56:06 +00:00
|
|
|
|
def suspend_service(self, service_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.post("/service/{}/suspend".format(service_id), data=None)
|
2017-01-31 15:56:06 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
2017-01-31 15:56:06 +00:00
|
|
|
|
def resume_service(self, service_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.post("/service/{}/resume".format(service_id), data=None)
|
2017-01-31 15:56:06 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
|
|
|
|
|
@cache.delete("user-{user_id}")
|
2016-03-23 10:46:31 +00:00
|
|
|
|
def remove_user_from_service(self, service_id, user_id):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Remove a user from a service
|
|
|
|
|
|
"""
|
2023-08-25 09:12:23 -07:00
|
|
|
|
endpoint = "/service/{service_id}/users/{user_id}".format(
|
|
|
|
|
|
service_id=service_id, user_id=user_id
|
|
|
|
|
|
)
|
2016-04-15 11:08:19 +01:00
|
|
|
|
data = _attach_current_user({})
|
|
|
|
|
|
return self.delete(endpoint, data)
|
2016-03-23 10:46:31 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}-templates")
|
|
|
|
|
|
def create_service_template(
|
|
|
|
|
|
self, name, type_, content, service_id, subject=None, parent_folder_id=None
|
|
|
|
|
|
):
|
2016-01-14 14:55:07 +00:00
|
|
|
|
"""
|
|
|
|
|
|
Create a service template.
|
|
|
|
|
|
"""
|
|
|
|
|
|
data = {
|
|
|
|
|
|
"name": name,
|
|
|
|
|
|
"template_type": type_,
|
|
|
|
|
|
"content": content,
|
2017-01-18 15:11:34 +00:00
|
|
|
|
"service": service_id,
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"process_type": "normal",
|
2016-01-14 14:55:07 +00:00
|
|
|
|
}
|
2016-02-22 14:14:03 +00:00
|
|
|
|
if subject:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data.update({"subject": subject})
|
2018-11-08 15:53:33 +00:00
|
|
|
|
if parent_folder_id:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data.update({"parent_folder_id": parent_folder_id})
|
2016-08-11 14:20:43 +01:00
|
|
|
|
data = _attach_current_user(data)
|
2016-01-14 14:55:07 +00:00
|
|
|
|
endpoint = "/service/{0}/template".format(service_id)
|
|
|
|
|
|
return self.post(endpoint, data)
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}-templates")
|
|
|
|
|
|
@cache.delete_by_pattern("service-{service_id}-template-*")
|
2018-12-19 10:58:35 +00:00
|
|
|
|
def update_service_template(
|
2023-07-03 11:43:16 -07:00
|
|
|
|
self, id_, name, type_, content, service_id, subject=None
|
2018-12-19 10:58:35 +00:00
|
|
|
|
):
|
2016-01-20 09:46:48 +00:00
|
|
|
|
"""
|
|
|
|
|
|
Update a service template.
|
|
|
|
|
|
"""
|
|
|
|
|
|
data = {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"id": id_,
|
|
|
|
|
|
"name": name,
|
|
|
|
|
|
"template_type": type_,
|
|
|
|
|
|
"content": content,
|
|
|
|
|
|
"service": service_id,
|
2016-01-20 09:46:48 +00:00
|
|
|
|
}
|
2016-02-22 14:14:03 +00:00
|
|
|
|
if subject:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data.update({"subject": subject})
|
|
|
|
|
|
data.update({"process_type": "normal"})
|
2016-08-11 14:20:43 +01:00
|
|
|
|
data = _attach_current_user(data)
|
2016-01-20 09:46:48 +00:00
|
|
|
|
endpoint = "/service/{0}/template/{1}".format(service_id, id_)
|
2022-02-22 12:28:23 +00:00
|
|
|
|
return self.post(endpoint, data)
|
2016-01-20 09:46:48 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}-templates")
|
|
|
|
|
|
@cache.delete_by_pattern("service-{service_id}-template-*")
|
2017-06-26 14:41:00 +01:00
|
|
|
|
def redact_service_template(self, service_id, id_):
|
2022-02-22 12:28:23 +00:00
|
|
|
|
return self.post(
|
2017-06-26 14:41:00 +01:00
|
|
|
|
"/service/{}/template/{}".format(service_id, id_),
|
2023-08-25 09:12:23 -07:00
|
|
|
|
_attach_current_user({"redact_personalisation": True}),
|
2017-06-26 14:41:00 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}-templates")
|
|
|
|
|
|
@cache.delete_by_pattern("service-{service_id}-template-*")
|
2018-01-03 10:44:36 +00:00
|
|
|
|
def update_service_template_sender(self, service_id, template_id, reply_to):
|
|
|
|
|
|
data = {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"reply_to": reply_to,
|
2018-01-03 10:44:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
data = _attach_current_user(data)
|
2022-02-22 12:28:23 +00:00
|
|
|
|
return self.post(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"/service/{0}/template/{1}".format(service_id, template_id), data
|
2018-01-03 10:44:36 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.set("service-{service_id}-template-{template_id}-version-{version}")
|
2018-04-19 10:26:17 +01:00
|
|
|
|
def get_service_template(self, service_id, template_id, version=None):
|
2016-01-19 10:23:08 +00:00
|
|
|
|
"""
|
|
|
|
|
|
Retrieve a service template.
|
|
|
|
|
|
"""
|
2023-08-25 09:12:23 -07:00
|
|
|
|
endpoint = "/service/{service_id}/template/{template_id}".format(
|
|
|
|
|
|
service_id=service_id, template_id=template_id
|
|
|
|
|
|
)
|
2016-05-11 11:20:45 +01:00
|
|
|
|
if version:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
endpoint = "{base}/version/{version}".format(base=endpoint, version=version)
|
2018-04-19 10:26:17 +01:00
|
|
|
|
return self.get(endpoint)
|
2016-05-11 11:20:45 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.set("service-{service_id}-template-{template_id}-versions")
|
2018-04-19 10:26:17 +01:00
|
|
|
|
def get_service_template_versions(self, service_id, template_id):
|
2016-05-11 11:20:45 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Retrieve a list of versions for a template
|
|
|
|
|
|
"""
|
2023-08-25 09:12:23 -07:00
|
|
|
|
endpoint = "/service/{service_id}/template/{template_id}/versions".format(
|
|
|
|
|
|
service_id=service_id, template_id=template_id
|
2016-05-11 11:20:45 +01:00
|
|
|
|
)
|
2018-04-19 10:26:17 +01:00
|
|
|
|
return self.get(endpoint)
|
2016-01-19 10:23:08 +00:00
|
|
|
|
|
2019-09-09 10:59:32 +01:00
|
|
|
|
def get_precompiled_template(self, service_id):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Returns the precompiled template for a service, creating it if it doesn't already exist
|
|
|
|
|
|
"""
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.get("/service/{}/template/precompiled".format(service_id))
|
2019-09-09 10:59:32 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.set("service-{service_id}-templates")
|
2018-04-19 10:26:17 +01:00
|
|
|
|
def get_service_templates(self, service_id):
|
2016-01-19 10:23:08 +00:00
|
|
|
|
"""
|
|
|
|
|
|
Retrieve all templates for service.
|
|
|
|
|
|
"""
|
2023-08-25 09:12:23 -07:00
|
|
|
|
endpoint = "/service/{service_id}/template?detailed=False".format(
|
|
|
|
|
|
service_id=service_id
|
|
|
|
|
|
)
|
2018-04-19 10:26:17 +01:00
|
|
|
|
return self.get(endpoint)
|
2016-01-19 10:23:08 +00:00
|
|
|
|
|
2018-04-19 10:24:35 +01:00
|
|
|
|
# This doesn’t need caching because it calls through to a method which is cached
|
2018-02-21 14:37:27 +00:00
|
|
|
|
def count_service_templates(self, service_id, template_type=None):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return len(
|
|
|
|
|
|
[
|
|
|
|
|
|
template
|
|
|
|
|
|
for template in self.get_service_templates(service_id)["data"]
|
|
|
|
|
|
if (not template_type or template["template_type"] == template_type)
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
2018-02-21 14:37:27 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}-templates")
|
|
|
|
|
|
@cache.delete_by_pattern("service-{service_id}-template-*")
|
2016-01-14 14:55:07 +00:00
|
|
|
|
def delete_service_template(self, service_id, template_id):
|
|
|
|
|
|
"""
|
2016-05-23 13:59:33 +01:00
|
|
|
|
Set a service template's archived flag to True
|
2016-01-14 14:55:07 +00:00
|
|
|
|
"""
|
|
|
|
|
|
endpoint = "/service/{0}/template/{1}".format(service_id, template_id)
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data = {"archived": True}
|
2016-08-11 14:20:43 +01:00
|
|
|
|
data = _attach_current_user(data)
|
2022-02-22 12:28:23 +00:00
|
|
|
|
return self.post(endpoint, data=data)
|
2016-03-29 22:50:40 +01:00
|
|
|
|
|
2016-04-21 17:34:01 +01:00
|
|
|
|
# Temp access of service history data. Includes service and api key history
|
|
|
|
|
|
def get_service_history(self, service_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.get("/service/{0}/history".format(service_id))["data"]
|
2019-10-18 16:09:39 +01:00
|
|
|
|
|
|
|
|
|
|
def get_service_service_history(self, service_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.get_service_history(service_id)["service_history"]
|
2019-10-18 16:09:39 +01:00
|
|
|
|
|
|
|
|
|
|
def get_service_api_key_history(self, service_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.get_service_history(service_id)["api_key_history"]
|
2016-04-21 17:34:01 +01:00
|
|
|
|
|
2017-01-30 17:27:09 +00:00
|
|
|
|
def get_monthly_notification_stats(self, service_id, year):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.get(
|
|
|
|
|
|
url="/service/{}/notifications/monthly?year={}".format(service_id, year)
|
|
|
|
|
|
)
|
2016-07-29 10:28:18 +01:00
|
|
|
|
|
2020-06-12 09:05:26 +01:00
|
|
|
|
def get_guest_list(self, service_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.get(url="/service/{}/guest-list".format(service_id))
|
2016-09-20 12:30:00 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
2020-06-12 09:05:26 +01:00
|
|
|
|
def update_guest_list(self, service_id, data):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.put(url="/service/{}/guest-list".format(service_id), data=data)
|
2016-09-20 12:30:00 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
def get_inbound_sms(self, service_id, user_number=""):
|
2019-03-29 15:16:29 +00:00
|
|
|
|
# POST prevents the user phone number leaking into our logs
|
|
|
|
|
|
return self.post(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"/service/{}/inbound-sms".format(
|
2017-05-24 13:19:31 +01:00
|
|
|
|
service_id,
|
2018-03-21 15:08:03 +00:00
|
|
|
|
),
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data={"phone_number": user_number},
|
2018-03-21 15:08:03 +00:00
|
|
|
|
)
|
2017-05-22 17:02:03 +01:00
|
|
|
|
|
2018-04-04 15:57:31 +01:00
|
|
|
|
def get_most_recent_inbound_sms(self, service_id, page=None):
|
|
|
|
|
|
return self.get(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"/service/{}/inbound-sms/most-recent".format(
|
2018-04-04 15:57:31 +01:00
|
|
|
|
service_id,
|
|
|
|
|
|
),
|
2023-08-25 09:12:23 -07:00
|
|
|
|
params={"page": page},
|
2018-04-04 15:57:31 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2017-05-24 13:19:31 +01:00
|
|
|
|
def get_inbound_sms_by_id(self, service_id, notification_id):
|
|
|
|
|
|
return self.get(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"/service/{}/inbound-sms/{}".format(
|
2017-05-24 13:19:31 +01:00
|
|
|
|
service_id,
|
|
|
|
|
|
notification_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2017-05-22 17:02:03 +01:00
|
|
|
|
def get_inbound_sms_summary(self, service_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.get("/service/{}/inbound-sms/summary".format(service_id))
|
2017-05-22 17:02:03 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
2017-06-20 13:29:20 +01:00
|
|
|
|
def create_service_inbound_api(self, service_id, url, bearer_token, user_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data = {"url": url, "bearer_token": bearer_token, "updated_by_id": user_id}
|
2017-06-20 13:29:20 +01:00
|
|
|
|
return self.post("/service/{}/inbound-api".format(service_id), data)
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
|
|
|
|
|
def update_service_inbound_api(
|
|
|
|
|
|
self, service_id, url, bearer_token, user_id, inbound_api_id
|
|
|
|
|
|
):
|
|
|
|
|
|
data = {"url": url, "updated_by_id": user_id}
|
2017-06-21 17:34:22 +01:00
|
|
|
|
if bearer_token:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data["bearer_token"] = bearer_token
|
|
|
|
|
|
return self.post(
|
|
|
|
|
|
"/service/{}/inbound-api/{}".format(service_id, inbound_api_id), data
|
|
|
|
|
|
)
|
2017-06-20 13:06:46 +01:00
|
|
|
|
|
|
|
|
|
|
def get_service_inbound_api(self, service_id, inbound_sms_api_id):
|
|
|
|
|
|
return self.get(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"/service/{}/inbound-api/{}".format(service_id, inbound_sms_api_id)
|
|
|
|
|
|
)["data"]
|
2017-06-15 16:20:07 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
2018-04-26 17:23:44 +01:00
|
|
|
|
def delete_service_inbound_api(self, service_id, callback_api_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.delete(
|
|
|
|
|
|
"/service/{}/inbound-api/{}".format(service_id, callback_api_id)
|
|
|
|
|
|
)
|
2018-04-26 17:23:44 +01:00
|
|
|
|
|
2017-09-25 15:14:13 +01:00
|
|
|
|
def get_reply_to_email_addresses(self, service_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.get("/service/{}/email-reply-to".format(service_id))
|
2017-09-25 15:14:13 +01:00
|
|
|
|
|
|
|
|
|
|
def get_reply_to_email_address(self, service_id, reply_to_email_id):
|
|
|
|
|
|
return self.get(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"/service/{}/email-reply-to/{}".format(service_id, reply_to_email_id)
|
2017-09-25 15:14:13 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2019-05-15 15:21:56 +01:00
|
|
|
|
def verify_reply_to_email_address(self, service_id, email_address):
|
2019-05-09 17:55:10 +01:00
|
|
|
|
return self.post(
|
2019-05-15 15:21:56 +01:00
|
|
|
|
"/service/{}/email-reply-to/verify".format(service_id),
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data={"email": email_address},
|
2019-05-09 17:55:10 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
|
|
|
|
|
@cache.delete_by_pattern("service-{service_id}-template-*")
|
2017-09-25 15:14:13 +01:00
|
|
|
|
def add_reply_to_email_address(self, service_id, email_address, is_default=False):
|
2022-02-22 12:28:23 +00:00
|
|
|
|
return self.post(
|
2017-09-25 15:14:13 +01:00
|
|
|
|
"/service/{}/email-reply-to".format(service_id),
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data={"email_address": email_address, "is_default": is_default},
|
2017-09-25 15:14:13 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
|
|
|
|
|
@cache.delete_by_pattern("service-{service_id}-template-*")
|
|
|
|
|
|
def update_reply_to_email_address(
|
|
|
|
|
|
self, service_id, reply_to_email_id, email_address, is_default=False
|
|
|
|
|
|
):
|
2022-02-22 12:28:23 +00:00
|
|
|
|
return self.post(
|
2017-09-25 15:14:13 +01:00
|
|
|
|
"/service/{}/email-reply-to/{}".format(
|
|
|
|
|
|
service_id,
|
|
|
|
|
|
reply_to_email_id,
|
|
|
|
|
|
),
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data={"email_address": email_address, "is_default": is_default},
|
2017-09-25 15:14:13 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
|
|
|
|
|
@cache.delete_by_pattern("service-{service_id}-template-*")
|
2018-04-26 15:09:37 +01:00
|
|
|
|
def delete_reply_to_email_address(self, service_id, reply_to_email_id):
|
2022-02-22 12:28:23 +00:00
|
|
|
|
return self.post(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"/service/{}/email-reply-to/{}/archive".format(
|
|
|
|
|
|
service_id, reply_to_email_id
|
|
|
|
|
|
),
|
|
|
|
|
|
data=None,
|
2018-04-26 15:09:37 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2017-10-24 15:37:44 +01:00
|
|
|
|
def get_sms_senders(self, service_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.get("/service/{}/sms-sender".format(service_id))
|
2017-10-24 15:37:44 +01:00
|
|
|
|
|
|
|
|
|
|
def get_sms_sender(self, service_id, sms_sender_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.get("/service/{}/sms-sender/{}".format(service_id, sms_sender_id))
|
2017-10-24 15:37:44 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
|
|
|
|
|
@cache.delete_by_pattern("service-{service_id}-template-*")
|
|
|
|
|
|
def add_sms_sender(
|
|
|
|
|
|
self, service_id, sms_sender, is_default=False, inbound_number_id=None
|
|
|
|
|
|
):
|
|
|
|
|
|
data = {"sms_sender": sms_sender, "is_default": is_default}
|
2017-10-24 15:37:44 +01:00
|
|
|
|
if inbound_number_id:
|
|
|
|
|
|
data["inbound_number_id"] = inbound_number_id
|
2022-02-22 12:28:23 +00:00
|
|
|
|
return self.post("/service/{}/sms-sender".format(service_id), data=data)
|
2017-10-24 15:37:44 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
|
|
|
|
|
@cache.delete_by_pattern("service-{service_id}-template-*")
|
|
|
|
|
|
def update_sms_sender(
|
|
|
|
|
|
self, service_id, sms_sender_id, sms_sender, is_default=False
|
|
|
|
|
|
):
|
2022-02-22 12:28:23 +00:00
|
|
|
|
return self.post(
|
2017-10-24 15:37:44 +01:00
|
|
|
|
"/service/{}/sms-sender/{}".format(service_id, sms_sender_id),
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data={"sms_sender": sms_sender, "is_default": is_default},
|
2018-04-26 15:09:37 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
|
|
|
|
|
@cache.delete_by_pattern("service-{service_id}-template-*")
|
2018-04-26 15:09:37 +01:00
|
|
|
|
def delete_sms_sender(self, service_id, sms_sender_id):
|
2022-02-22 12:28:23 +00:00
|
|
|
|
return self.post(
|
2018-04-26 15:09:37 +01:00
|
|
|
|
"/service/{}/sms-sender/{}/archive".format(service_id, sms_sender_id),
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data=None,
|
2017-10-24 15:37:44 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2017-12-04 15:07:11 +00:00
|
|
|
|
def get_service_callback_api(self, service_id, callback_api_id):
|
|
|
|
|
|
return self.get(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"/service/{}/delivery-receipt-api/{}".format(service_id, callback_api_id)
|
|
|
|
|
|
)["data"]
|
2017-12-04 15:07:11 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
|
|
|
|
|
def update_service_callback_api(
|
|
|
|
|
|
self, service_id, url, bearer_token, user_id, callback_api_id
|
|
|
|
|
|
):
|
|
|
|
|
|
data = {"url": url, "updated_by_id": user_id}
|
2017-12-04 15:07:11 +00:00
|
|
|
|
if bearer_token:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data["bearer_token"] = bearer_token
|
|
|
|
|
|
return self.post(
|
|
|
|
|
|
"/service/{}/delivery-receipt-api/{}".format(service_id, callback_api_id),
|
|
|
|
|
|
data,
|
|
|
|
|
|
)
|
2017-12-04 15:07:11 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
2018-04-26 17:23:44 +01:00
|
|
|
|
def delete_service_callback_api(self, service_id, callback_api_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return self.delete(
|
|
|
|
|
|
"/service/{}/delivery-receipt-api/{}".format(service_id, callback_api_id)
|
|
|
|
|
|
)
|
2018-04-26 17:23:44 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}")
|
2017-12-04 15:07:11 +00:00
|
|
|
|
def create_service_callback_api(self, service_id, url, bearer_token, user_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data = {"url": url, "bearer_token": bearer_token, "updated_by_id": user_id}
|
2017-12-04 15:07:11 +00:00
|
|
|
|
return self.post("/service/{}/delivery-receipt-api".format(service_id), data)
|
2018-07-13 16:47:26 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}-data-retention")
|
|
|
|
|
|
def create_service_data_retention(
|
|
|
|
|
|
self, service_id, notification_type, days_of_retention
|
|
|
|
|
|
):
|
2018-07-13 16:47:26 +01:00
|
|
|
|
data = {
|
|
|
|
|
|
"notification_type": notification_type,
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"days_of_retention": days_of_retention,
|
2018-07-13 16:47:26 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return self.post("/service/{}/data-retention".format(service_id), data)
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.delete("service-{service_id}-data-retention")
|
|
|
|
|
|
def update_service_data_retention(
|
|
|
|
|
|
self, service_id, data_retention_id, days_of_retention
|
|
|
|
|
|
):
|
|
|
|
|
|
data = {"days_of_retention": days_of_retention}
|
|
|
|
|
|
return self.post(
|
|
|
|
|
|
"/service/{}/data-retention/{}".format(service_id, data_retention_id), data
|
|
|
|
|
|
)
|
2018-07-13 16:47:26 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@cache.set("service-{service_id}-data-retention")
|
2018-07-13 16:47:26 +01:00
|
|
|
|
def get_service_data_retention(self, service_id):
|
|
|
|
|
|
return self.get("/service/{}/data-retention".format(service_id))
|
|
|
|
|
|
|
2021-06-23 15:56:05 +01:00
|
|
|
|
def get_notification_count(self, service_id):
|
2023-07-11 12:37:27 -07:00
|
|
|
|
count = 0
|
2022-09-08 09:07:54 -07:00
|
|
|
|
|
2021-06-23 15:56:05 +01:00
|
|
|
|
return int(count)
|
|
|
|
|
|
|
2023-10-10 10:59:46 -06:00
|
|
|
|
def get_global_notification_count(self, service_id):
|
2025-01-16 08:38:51 -08:00
|
|
|
|
notification_count = redis_client.get(f"notification-count-{service_id}")
|
|
|
|
|
|
if notification_count is not None:
|
|
|
|
|
|
return json.loads(notification_count.decode("utf-8"))
|
|
|
|
|
|
|
|
|
|
|
|
notification_count = self.get(
|
|
|
|
|
|
"/service/{}/notification-count".format(service_id)
|
|
|
|
|
|
)
|
|
|
|
|
|
redis_client.set(
|
|
|
|
|
|
f"notification-count-{service_id}", json.dumps(notification_count), ex=30
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return notification_count
|
2023-06-01 10:44:13 -06:00
|
|
|
|
|
2024-04-19 13:53:32 -07:00
|
|
|
|
def get_service_invite_data(self, redis_key):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Retrieve service invite_data.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return self.get("/service/invite/redis/{0}".format(redis_key))
|
|
|
|
|
|
|
2025-02-24 14:08:26 -08:00
|
|
|
|
def get_service_message_ratio(self, service_id):
|
|
|
|
|
|
return self.get(
|
|
|
|
|
|
url="service/get-service-message-ratio?service_id={0}".format(service_id),
|
|
|
|
|
|
)
|
2019-12-18 11:53:46 +00:00
|
|
|
|
|
2025-02-25 13:19:59 -08:00
|
|
|
|
|
2018-10-26 15:39:32 +01:00
|
|
|
|
service_api_client = ServiceAPIClient()
|
2024-04-19 13:53:32 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# TODO, if we try to call get_service_invite_data directly
|
|
|
|
|
|
# from verify, app complains the method is not defined
|
|
|
|
|
|
# If we wrap it like this, the app can find it.
|
|
|
|
|
|
def retrieve_service_invite_data(redis_key):
|
|
|
|
|
|
return service_api_client.get_service_invite_data(redis_key)
|