Compare commits

...

13 Commits

Author SHA1 Message Date
Katie Smith
86e286433f Update schemas for breaking changes in flask-marshmallow 0.12.0
See https://flask-marshmallow.readthedocs.io/en/latest/changelog.html#id2
2020-06-16 16:19:51 +01:00
Leo Hemsted
eec2c2859e Merge pull request #2876 from alphagov/more-histograms
add more prometheus metrics
2020-06-15 17:28:59 +01:00
Leo Hemsted
58ab99d74b add more prometheus metrics
Two new metrics:

auth_db_connection_duration_seconds (histogram)
  wraps the first DB call of post notifications. This includes waiting
  to get a connection from the pool, and also making the actual request
  to the db to retrieve the service and api keys. (i'm not sure there's
  an easy way to separate these two things)

post_notification_json_parse_duration_seconds
  wraps parsing the v2 post notifications json parsing and schema
  validation. Shouldn't include any async code
2020-06-15 16:26:56 +01:00
David McDonald
4c230a7235 Merge pull request #2819 from alphagov/additional-prometheus-metrics
Additional prometheus metrics
2020-06-12 17:02:12 +01:00
David McDonald
8b4a424df1 Tidy up 2020-06-12 16:51:44 +01:00
Leo Hemsted
15ce9fe3f9 add metrics for redis timings 2020-06-12 14:52:22 +01:00
Leo Hemsted
d9b3b31a6a add loadtesting to manifest so we can deploy a separate app 2020-06-12 14:52:22 +01:00
Leo Hemsted
cd9b80f415 set test_errors app fixture to session scope
we have one global metrics variable `metrics = GDSMetrics()`, and we
then call `metrics.init_app` from within the flask application set up.
The v2/test_errors.py app_for_test fixture calls create_app, would call
metrics.init_app multiple times for the same metrics instance. This
causes errors, so change the fixture to session level so it only calls
once per test run.
2020-06-12 14:52:22 +01:00
Leo Hemsted
faa8faa0c4 bump prometheus-client to 0.7.1
there's multiple performance improvements from prometheus-client 0.2.0.
pin this bump while we wait for gds metrics client to increase its
dependency
2020-06-12 14:52:22 +01:00
Leo Hemsted
c4dc0f64c5 dd sqlalchemy connection metrics for celery tasks
grab the worker app name and task name rather than the web host and
endpoint. also add a fallback for if we're not in a web request or a
celery task. I think that'll probably happen when we use alembic, or if
we do things from within flask shell
2020-06-12 14:52:22 +01:00
Leo Hemsted
6e32ca5996 add prometheus metrics for connection pools (both web and sql)
add the following prometheus metrics to keep track of general app
instance health.

 # sqs_apply_async_duration

how long does the actual SQS call (a standard web request to AWS) take.
a histogram with default bucket sizes, split up per task that was
created.

 # concurrent_web_request_count

how many web requests is this app currently serving. this is split up
per process, so we'd expect multiple responses per app instance

 # db_connection_total_connected

how many connections does this app (process) have open to the database.
They might be idle.

 # db_connection_total_checked_out

how many connections does this app (process) have open that are
currently in use by a web worker

 # db_connection_open_duration_seconds

a histogram per endpoint of how long the db connection was taken from
the pool for. won't have any data if a connection was never opened.
2020-06-12 14:52:22 +01:00
Leo Hemsted
4bb37a05ec Merge pull request #2871 from alphagov/utils-bump
bump utils to fix statsd timing bug
2020-06-11 15:13:04 +01:00
Leo Hemsted
bd433ad24f bump utils to fix statsd timing bug
statsd timing should always be in seconds, not milliseconds
2020-06-11 15:01:15 +01:00
11 changed files with 213 additions and 67 deletions

View File

@@ -1,19 +1,23 @@
import time
import os
import random
import string
import uuid
from flask import _request_ctx_stack, request, g, jsonify, make_response
from celery import current_task
from flask import _request_ctx_stack, request, g, jsonify, make_response, current_app, has_request_context
from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy
from flask_marshmallow import Marshmallow
from flask_migrate import Migrate
from gds_metrics import GDSMetrics
from gds_metrics.metrics import Gauge, Histogram
from time import monotonic
from notifications_utils.clients.zendesk.zendesk_client import ZendeskClient
from notifications_utils.clients.statsd.statsd_client import StatsdClient
from notifications_utils.clients.redis.redis_client import RedisClient
from notifications_utils.clients.encryption.encryption_client import Encryption
from notifications_utils import logging, request_helper
from sqlalchemy import event
from werkzeug.exceptions import HTTPException as WerkzeugHTTPException
from werkzeug.local import LocalProxy
@@ -64,6 +68,11 @@ clients = Clients()
api_user = LocalProxy(lambda: _request_ctx_stack.top.api_user)
authenticated_service = LocalProxy(lambda: _request_ctx_stack.top.authenticated_service)
CONCURRENT_REQUESTS = Gauge(
'concurrent_web_request_count',
'How many concurrent requests are currently being served',
)
def create_app(application):
from app.config import configs
@@ -108,6 +117,9 @@ def create_app(application):
from app.commands import setup_commands
setup_commands(application)
# set up sqlalchemy events
setup_sqlalchemy_events(application)
return application
@@ -255,17 +267,22 @@ def register_v2_blueprints(application):
def init_app(app):
@app.before_request
def record_user_agent():
statsd_client.incr("user-agent.{}".format(process_user_agent(request.headers.get('User-Agent', None))))
@app.before_request
def record_request_details():
CONCURRENT_REQUESTS.inc()
g.start = monotonic()
g.endpoint = request.endpoint
@app.after_request
def after_request(response):
CONCURRENT_REQUESTS.dec()
response.headers.add('Access-Control-Allow-Origin', '*')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
@@ -311,3 +328,83 @@ def process_user_agent(user_agent_string):
return "non-notify-user-agent"
else:
return "unknown"
def setup_sqlalchemy_events(app):
TOTAL_DB_CONNECTIONS = Gauge(
'db_connection_total_connected',
'How many db connections are currently held (potentially idle) by the server',
)
TOTAL_CHECKED_OUT_DB_CONNECTIONS = Gauge(
'db_connection_total_checked_out',
'How many db connections are currently checked out by web requests',
)
DB_CONNECTION_OPEN_DURATION_SECONDS = Histogram(
'db_connection_open_duration_seconds',
'How long db connections are held open for in seconds',
['method', 'host', 'path']
)
# need this or db.engine isn't accessible
with app.app_context():
@event.listens_for(db.engine, 'connect')
def connect(dbapi_connection, connection_record):
# connection first opened with db
TOTAL_DB_CONNECTIONS.inc()
@event.listens_for(db.engine, 'close')
def close(dbapi_connection, connection_record):
# connection closed (probably only happens with overflow connections)
TOTAL_DB_CONNECTIONS.dec()
@event.listens_for(db.engine, 'checkout')
def checkout(dbapi_connection, connection_record, connection_proxy):
# connection given to a web worker
TOTAL_CHECKED_OUT_DB_CONNECTIONS.inc()
# this will overwrite any previous checkout_at timestamp
connection_record.info['checkout_at'] = time.monotonic()
# checkin runs after the request is already torn down, therefore we add the request_data onto the
# connection_record as otherwise it won't have that information when checkin actually runs.
# Note: this is not a problem for checkouts as the checkout always happens within a web request or task
# web requests
if has_request_context():
connection_record.info['request_data'] = {
'method': request.method,
'host': request.host,
'url_rule': request.url_rule.rule if request.url_rule else 'No endpoint'
}
# celery apps
elif current_task:
connection_record.info['request_data'] = {
'method': 'celery',
'host': current_app.config['NOTIFY_APP_NAME'], # worker name
'url_rule': current_task.name, # task name
}
# anything else. migrations possibly.
else:
current_app.logger.warning('Checked out sqlalchemy connection from outside of request/task')
connection_record.info['request_data'] = {
'method': 'unknown',
'host': 'unknown',
'url_rule': 'unknown',
}
@event.listens_for(db.engine, 'checkin')
def checkin(dbapi_connection, connection_record):
# connection returned by a web worker
TOTAL_CHECKED_OUT_DB_CONNECTIONS.dec()
# duration that connection was held by a single web request
duration = time.monotonic() - connection_record.info['checkout_at']
DB_CONNECTION_OPEN_DURATION_SECONDS.labels(
connection_record.info['request_data']['method'],
connection_record.info['request_data']['host'],
connection_record.info['request_data']['url_rule']
).observe(duration)

View File

@@ -6,12 +6,18 @@ from notifications_python_client.errors import (
from notifications_utils import request_helper
from sqlalchemy.exc import DataError
from sqlalchemy.orm.exc import NoResultFound
from gds_metrics import Histogram
from app.dao.services_dao import dao_fetch_service_by_id_with_api_keys
GENERAL_TOKEN_ERROR_MESSAGE = 'Invalid token: make sure your API token matches the example at https://docs.notifications.service.gov.uk/rest-api.html#authorisation-header' # noqa
AUTH_DB_CONNECTION_DURATION_SECONDS = Histogram(
'auth_db_connection_duration_seconds',
'Time taken to get DB connection and fetch service from database',
)
class AuthError(Exception):
def __init__(self, message, code, service_id=None, api_key_id=None):
@@ -87,7 +93,8 @@ def requires_auth():
issuer = __get_token_issuer(auth_token) # ie the `iss` claim which should be a service ID
try:
service = dao_fetch_service_by_id_with_api_keys(issuer)
with AUTH_DB_CONNECTION_DURATION_SECONDS.time():
service = dao_fetch_service_by_id_with_api_keys(issuer)
except DataError:
raise AuthError("Invalid token: service id is not the right data type", 403)
except NoResultFound:

View File

@@ -1,5 +1,6 @@
import time
from gds_metrics.metrics import Histogram
from celery import Celery, Task
from celery.signals import worker_process_shutdown
from flask import g, request
@@ -19,6 +20,12 @@ def log_on_worker_shutdown(sender, signal, pid, exitcode, **kwargs):
def make_task(app):
SQS_APPLY_ASYNC_DURATION_SECONDS = Histogram(
'sqs_apply_async_duration_seconds',
'Time taken to put task on queue',
['task_name']
)
class NotifyTask(Task):
abstract = True
start = None
@@ -52,7 +59,8 @@ def make_task(app):
if has_request_context() and hasattr(request, 'request_id'):
kwargs['request_id'] = request.request_id
return super().apply_async(args, kwargs, task_id, producer, link, link_error, **options)
with SQS_APPLY_ASYNC_DURATION_SECONDS.labels(self.name).time():
return super().apply_async(args, kwargs, task_id, producer, link, link_error, **options)
return NotifyTask

View File

@@ -72,8 +72,8 @@ def send_sms_to_provider(notification):
notification.billable_units = template.fragment_count
update_notification_to_sending(notification, provider)
delta_milliseconds = (datetime.utcnow() - notification.created_at).total_seconds() * 1000
statsd_client.timing("sms.total-time", delta_milliseconds)
delta_seconds = (datetime.utcnow() - notification.created_at).total_seconds()
statsd_client.timing("sms.total-time", delta_seconds)
def send_email_to_provider(notification):
@@ -118,8 +118,8 @@ def send_email_to_provider(notification):
notification.reference = reference
update_notification_to_sending(notification, provider)
delta_milliseconds = (datetime.utcnow() - notification.created_at).total_seconds() * 1000
statsd_client.timing("email.total-time", delta_milliseconds)
delta_seconds = (datetime.utcnow() - notification.created_at).total_seconds()
statsd_client.timing("email.total-time", delta_seconds)
def update_notification_to_sending(notification, provider):

View File

@@ -34,6 +34,15 @@ from app.dao.notifications_dao import (
from app.v2.errors import BadRequestError
from gds_metrics import Histogram
REDIS_GET_AND_INCR_DAILY_LIMIT_DURATION_SECONDS = Histogram(
'redis_get_and_incr_daily_limit_duration_seconds',
'Time taken to get and possibly incremement the daily limit cache key',
)
def create_content_for_notification(template, personalisation):
template_object = template._as_utils_template_with_personalisation(personalisation)
check_placeholders(template_object)
@@ -115,8 +124,9 @@ def persist_notification(
if not simulated:
dao_create_notification(notification)
if key_type != KEY_TYPE_TEST:
if redis_store.get(redis.daily_limit_cache_key(service.id)):
redis_store.incr(redis.daily_limit_cache_key(service.id))
with REDIS_GET_AND_INCR_DAILY_LIMIT_DURATION_SECONDS.time():
if redis_store.get(redis.daily_limit_cache_key(service.id)):
redis_store.incr(redis.daily_limit_cache_key(service.id))
current_app.logger.info(
"{} {} created at {}".format(notification_type, notification_id, notification_created_at)

View File

@@ -22,15 +22,24 @@ from app.utils import get_public_notify_type_text
from app.dao.service_email_reply_to_dao import dao_get_reply_to_by_id
from app.dao.service_letter_contact_dao import dao_get_letter_contact_by_id
from gds_metrics.metrics import Histogram
REDIS_EXCEEDED_RATE_LIMIT_DURATION_SECONDS = Histogram(
'redis_exceeded_rate_limit_duration_seconds',
'Time taken to check rate limit',
)
def check_service_over_api_rate_limit(service, api_key):
if current_app.config['API_RATE_LIMIT_ENABLED'] and current_app.config['REDIS_ENABLED']:
cache_key = rate_limit_cache_key(service.id, api_key.key_type)
rate_limit = service.rate_limit
interval = 60
if redis_store.exceeded_rate_limit(cache_key, rate_limit, interval):
current_app.logger.info("service {} has been rate limited for throughput".format(service.id))
raise RateLimitError(rate_limit, interval, api_key.key_type)
with REDIS_EXCEEDED_RATE_LIMIT_DURATION_SECONDS.time():
if redis_store.exceeded_rate_limit(cache_key, rate_limit, interval):
current_app.logger.info("service {} has been rate limited for throughput".format(service.id))
raise RateLimitError(rate_limit, interval, api_key.key_type)
def check_service_over_daily_message_limit(key_type, service):

View File

@@ -12,7 +12,7 @@ from marshmallow import (
pre_dump,
post_dump
)
from marshmallow_sqlalchemy import field_for
from marshmallow_sqlalchemy import auto_field
from notifications_utils.recipients import (
validate_email_address,
@@ -58,7 +58,7 @@ def _validate_datetime_not_in_past(dte, msg="Date cannot be in the past"):
raise ValidationError(msg)
class BaseSchema(ma.ModelSchema):
class BaseSchema(ma.SQLAlchemyAutoSchema):
def __init__(self, load_json=False, *args, **kwargs):
self.load_json = load_json
@@ -75,13 +75,17 @@ class BaseSchema(ma.ModelSchema):
return data
return super(BaseSchema, self).make_instance(data)
class Meta:
include_relationships = True
load_instance = True
class UserSchema(BaseSchema):
permissions = fields.Method("user_permissions", dump_only=True)
password_changed_at = field_for(models.User, 'password_changed_at', format=DATETIME_FORMAT_NO_TIMEZONE)
created_at = field_for(models.User, 'created_at', format=DATETIME_FORMAT_NO_TIMEZONE)
auth_type = field_for(models.User, 'auth_type')
password_changed_at = auto_field(format=DATETIME_FORMAT_NO_TIMEZONE)
created_at = auto_field(format=DATETIME_FORMAT_NO_TIMEZONE)
auth_type = auto_field()
def user_permissions(self, usr):
retval = {}
@@ -92,7 +96,7 @@ class UserSchema(BaseSchema):
retval[service_id].append(x.permission)
return retval
class Meta:
class Meta(BaseSchema.Meta):
model = models.User
exclude = (
"updated_at",
@@ -127,9 +131,9 @@ class UserSchema(BaseSchema):
class UserUpdateAttributeSchema(BaseSchema):
auth_type = field_for(models.User, 'auth_type')
auth_type = auto_field()
class Meta:
class Meta(BaseSchema.Meta):
model = models.User
exclude = (
'id', 'updated_at', 'created_at', 'user_to_service',
@@ -166,7 +170,7 @@ class UserUpdateAttributeSchema(BaseSchema):
class UserUpdatePasswordSchema(BaseSchema):
class Meta:
class Meta(BaseSchema.Meta):
model = models.User
only = ('password')
strict = True
@@ -181,7 +185,7 @@ class UserUpdatePasswordSchema(BaseSchema):
class ProviderDetailsSchema(BaseSchema):
created_by = fields.Nested(UserSchema, only=['id', 'name', 'email_address'], dump_only=True)
class Meta:
class Meta(BaseSchema.Meta):
model = models.ProviderDetails
exclude = ("provider_rates", "provider_stats")
strict = True
@@ -190,7 +194,7 @@ class ProviderDetailsSchema(BaseSchema):
class ProviderDetailsHistorySchema(BaseSchema):
created_by = fields.Nested(UserSchema, only=['id', 'name', 'email_address'], dump_only=True)
class Meta:
class Meta(BaseSchema.Meta):
model = models.ProviderDetailsHistory
exclude = ("provider_rates", "provider_stats")
strict = True
@@ -198,15 +202,15 @@ class ProviderDetailsHistorySchema(BaseSchema):
class ServiceSchema(BaseSchema):
created_by = field_for(models.Service, 'created_by', required=True)
organisation_type = field_for(models.Service, 'organisation_type')
created_by = auto_field(required=True)
organisation_type = auto_field()
letter_logo_filename = fields.Method(dump_only=True, serialize='get_letter_logo_filename')
permissions = fields.Method("service_permissions")
email_branding = field_for(models.Service, 'email_branding')
organisation = field_for(models.Service, 'organisation')
email_branding = auto_field()
organisation = auto_field()
override_flag = False
letter_contact_block = fields.Method(serialize="get_letter_contact")
go_live_at = field_for(models.Service, 'go_live_at', format=DATETIME_FORMAT_NO_TIMEZONE)
go_live_at = auto_field(format=DATETIME_FORMAT_NO_TIMEZONE)
def get_letter_logo_filename(self, service):
return service.letter_branding and service.letter_branding.filename
@@ -217,7 +221,7 @@ class ServiceSchema(BaseSchema):
def get_letter_contact(self, service):
return service.get_default_letter_contact()
class Meta:
class Meta(BaseSchema.Meta):
model = models.Service
dump_only = ['letter_contact_block']
exclude = (
@@ -263,9 +267,9 @@ class ServiceSchema(BaseSchema):
class DetailedServiceSchema(BaseSchema):
statistics = fields.Dict()
organisation_type = field_for(models.Service, 'organisation_type')
organisation_type = auto_field()
class Meta:
class Meta(BaseSchema.Meta):
model = models.Service
exclude = (
'api_keys',
@@ -294,7 +298,7 @@ class DetailedServiceSchema(BaseSchema):
class NotificationModelSchema(BaseSchema):
class Meta:
class Meta(BaseSchema.Meta):
model = models.Notification
strict = True
exclude = ('_personalisation', 'job', 'service', 'template', 'api_key',)
@@ -312,7 +316,7 @@ class BaseTemplateSchema(BaseSchema):
def get_reply_to_text(self, template):
return template.get_reply_to_text()
class Meta:
class Meta(BaseSchema.Meta):
model = models.Template
exclude = ("service_id", "jobs", "service_letter_contact_id")
strict = True
@@ -320,8 +324,8 @@ class BaseTemplateSchema(BaseSchema):
class TemplateSchema(BaseTemplateSchema):
created_by = field_for(models.Template, 'created_by', required=True)
process_type = field_for(models.Template, 'process_type')
created_by = auto_field(required=True)
process_type = auto_field()
redact_personalisation = fields.Method("redact")
def redact(self, template):
@@ -341,7 +345,7 @@ class TemplateHistorySchema(BaseSchema):
reply_to_text = fields.Method("get_reply_to_text", allow_none=True)
created_by = fields.Nested(UserSchema, only=['id', 'name', 'email_address'], dump_only=True)
created_at = field_for(models.Template, 'created_at', format=DATETIME_FORMAT_NO_TIMEZONE)
created_at = auto_field(format=DATETIME_FORMAT_NO_TIMEZONE)
def get_reply_to(self, template):
return template.reply_to
@@ -349,16 +353,16 @@ class TemplateHistorySchema(BaseSchema):
def get_reply_to_text(self, template):
return template.get_reply_to_text()
class Meta:
class Meta(BaseSchema.Meta):
model = models.TemplateHistory
class ApiKeySchema(BaseSchema):
created_by = field_for(models.ApiKey, 'created_by', required=True)
key_type = field_for(models.ApiKey, 'key_type', required=True)
created_by = auto_field(required=True)
key_type = auto_field(required=True)
class Meta:
class Meta(BaseSchema.Meta):
model = models.ApiKey
exclude = ("service", "_secret")
strict = True
@@ -367,9 +371,9 @@ class ApiKeySchema(BaseSchema):
class JobSchema(BaseSchema):
created_by_user = fields.Nested(UserSchema, attribute="created_by",
dump_to="created_by", only=["id", "name"], dump_only=True)
created_by = field_for(models.Job, 'created_by', required=True, load_only=True)
created_by = auto_field(required=True, load_only=True)
job_status = field_for(models.JobStatus, 'name', required=False)
job_status = auto_field("name", model=models.JobStatus, required=False)
scheduled_for = fields.DateTime()
service_name = fields.Nested(
@@ -377,7 +381,7 @@ class JobSchema(BaseSchema):
template_name = fields.Method('get_template_name', dump_only=True)
template_type = fields.Method('get_template_type', dump_only=True)
contact_list_id = field_for(models.Job, 'contact_list_id')
contact_list_id = auto_field()
def get_template_name(self, job):
return job.template.name
@@ -390,7 +394,7 @@ class JobSchema(BaseSchema):
_validate_datetime_not_in_past(value)
_validate_datetime_not_more_than_96_hours_in_future(value)
class Meta:
class Meta(BaseSchema.Meta):
model = models.Job
exclude = (
'notifications',
@@ -402,7 +406,7 @@ class JobSchema(BaseSchema):
class NotificationSchema(ma.Schema):
class Meta:
class Meta(BaseSchema.Meta):
strict = True
status = fields.String(required=False)
@@ -443,7 +447,7 @@ class SmsTemplateNotificationSchema(SmsNotificationSchema):
class NotificationWithTemplateSchema(BaseSchema):
class Meta:
class Meta(BaseSchema.Meta):
model = models.Notification
strict = True
exclude = ('_personalisation', 'scheduled_notification')
@@ -466,7 +470,7 @@ class NotificationWithTemplateSchema(BaseSchema):
created_by = fields.Nested(UserSchema, only=['id', 'name', 'email_address'], dump_only=True)
status = fields.String(required=False)
personalisation = fields.Dict(required=False)
key_type = field_for(models.Notification, 'key_type', required=True)
key_type = auto_field(required=True)
key_name = fields.String()
@pre_dump
@@ -520,9 +524,9 @@ class NotificationWithPersonalisationSchema(NotificationWithTemplateSchema):
class InvitedUserSchema(BaseSchema):
auth_type = field_for(models.InvitedUser, 'auth_type')
auth_type = auto_field()
class Meta:
class Meta(BaseSchema.Meta):
model = models.InvitedUser
strict = True
@@ -536,7 +540,7 @@ class InvitedUserSchema(BaseSchema):
class EmailDataSchema(ma.Schema):
class Meta:
class Meta(BaseSchema.Meta):
strict = True
email = fields.Str(required=True)
@@ -557,7 +561,7 @@ class EmailDataSchema(ma.Schema):
class NotificationsFilterSchema(ma.Schema):
class Meta:
class Meta(BaseSchema.Meta):
strict = True
template_type = fields.Nested(BaseTemplateSchema, only=['template_type'], many=True)
@@ -625,7 +629,7 @@ class ApiKeyHistorySchema(ma.Schema):
class EventSchema(BaseSchema):
class Meta:
class Meta(BaseSchema.Meta):
model = models.Event
strict = True

View File

@@ -7,6 +7,7 @@ from boto.exception import SQSError
from flask import request, jsonify, current_app, abort
from notifications_utils.postal_address import PostalAddress
from notifications_utils.recipients import try_validate_and_format_phone_number
from gds_metrics import Histogram
from app import (
api_user,
@@ -74,6 +75,12 @@ from app.v2.notifications.notification_schemas import (
from app.v2.utils import get_valid_json
POST_NOTIFICATION_JSON_PARSE_DURATION_SECONDS = Histogram(
'post_notification_json_parse_duration_seconds',
'Time taken to parse and validate post request json',
)
@v2_notification_blueprint.route('/{}'.format(LETTER_TYPE), methods=['POST'])
def post_precompiled_letter_notification():
request_json = get_valid_json()
@@ -116,16 +123,17 @@ def post_precompiled_letter_notification():
@v2_notification_blueprint.route('/<notification_type>', methods=['POST'])
def post_notification(notification_type):
request_json = get_valid_json()
with POST_NOTIFICATION_JSON_PARSE_DURATION_SECONDS.time():
request_json = get_valid_json()
if notification_type == EMAIL_TYPE:
form = validate(request_json, post_email_request)
elif notification_type == SMS_TYPE:
form = validate(request_json, post_sms_request)
elif notification_type == LETTER_TYPE:
form = validate(request_json, post_letter_request)
else:
abort(404)
if notification_type == EMAIL_TYPE:
form = validate(request_json, post_email_request)
elif notification_type == SMS_TYPE:
form = validate(request_json, post_sms_request)
elif notification_type == LETTER_TYPE:
form = validate(request_json, post_letter_request)
else:
abort(404)
check_service_has_permission(notification_type, authenticated_service.permissions)

View File

@@ -5,7 +5,7 @@ cffi==1.14.0
celery[sqs]==3.1.26.post2 # pyup: <4
docopt==0.6.2
Flask-Bcrypt==0.7.1
flask-marshmallow==0.11.0
flask-marshmallow==0.12.0
Flask-Migrate==2.5.3
git+https://github.com/mitsuhiko/flask-sqlalchemy.git@500e732dd1b975a56ab06a46bd1a20a21e682262#egg=Flask-SQLAlchemy==2.3.2.dev20190108
Flask==1.1.2
@@ -26,6 +26,8 @@ notifications-python-client==5.5.1
# PaaS
awscli-cwlogs>=1.4,<1.5
git+https://github.com/alphagov/notifications-utils.git@39.4.3#egg=notifications-utils==39.4.3
git+https://github.com/alphagov/notifications-utils.git@39.4.4#egg=notifications-utils==39.4.4
# gds-metrics requires prometheseus 0.2.0, override that requirement as 0.7.1 brings significant performance gains
prometheus-client==0.7.1
gds-metrics==0.2.0

View File

@@ -7,7 +7,7 @@ cffi==1.14.0
celery[sqs]==3.1.26.post2 # pyup: <4
docopt==0.6.2
Flask-Bcrypt==0.7.1
flask-marshmallow==0.11.0
flask-marshmallow==0.12.0
Flask-Migrate==2.5.3
git+https://github.com/mitsuhiko/flask-sqlalchemy.git@500e732dd1b975a56ab06a46bd1a20a21e682262#egg=Flask-SQLAlchemy==2.3.2.dev20190108
Flask==1.1.2
@@ -28,8 +28,10 @@ notifications-python-client==5.5.1
# PaaS
awscli-cwlogs>=1.4,<1.5
git+https://github.com/alphagov/notifications-utils.git@39.4.3#egg=notifications-utils==39.4.3
git+https://github.com/alphagov/notifications-utils.git@39.4.4#egg=notifications-utils==39.4.4
# gds-metrics requires prometheseus 0.2.0, override that requirement as 0.7.1 brings significant performance gains
prometheus-client==0.7.1
gds-metrics==0.2.0
## The following requirements were added by pip freeze:
@@ -66,7 +68,6 @@ mistune==0.8.4
monotonic==1.5
orderedset==2.0.1
phonenumbers==8.11.2
prometheus-client==0.2.0
pyasn1==0.4.8
pycparser==2.20
PyPDF2==1.26.0

View File

@@ -4,7 +4,7 @@ from sqlalchemy.exc import DataError
@pytest.fixture(scope='function')
def app_for_test(mocker):
def app_for_test():
import flask
from flask import Blueprint
from app.authentication.auth import AuthError